ssh-licco 1.3.2__tar.gz → 1.3.3__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.
- {ssh_licco-1.3.2/ssh_licco.egg-info → ssh_licco-1.3.3}/PKG-INFO +13 -9
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/README.md +12 -8
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/pyproject.toml +1 -1
- {ssh_licco-1.3.2 → ssh_licco-1.3.3/ssh_licco.egg-info}/PKG-INFO +13 -9
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/__init__.py +2 -2
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/server.py +141 -72
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/session_manager.py +16 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/LICENSE +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/setup.cfg +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/SOURCES.txt +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/dependency_links.txt +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/entry_points.txt +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/requires.txt +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/top_level.txt +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/audit_logger.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/batch_executor.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/cli.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/__init__.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/additional_clients.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/factory.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/interface.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/paramiko_client.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/config_manager.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/connection_config.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/connection_pool.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/exceptions.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/executor.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/fallback_executor.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/key_manager.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/logging_config.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/security.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/service.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/watchdog.py +0 -0
- {ssh_licco-1.3.2 → ssh_licco-1.3.3}/tests/test_session_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssh-licco
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: SSH Model Context Protocol Server - Enable SSH functionality for AI models
|
|
5
5
|
Author-email: SSH LICCO Team <contact@ssh-licco.dev>
|
|
6
6
|
License: MIT
|
|
@@ -53,7 +53,6 @@ Dynamic: license-file
|
|
|
53
53
|
|
|
54
54
|
### 核心功能
|
|
55
55
|
- **[🔐 安全配置](#-安全配置)** - 多级安全策略
|
|
56
|
-
- **[🐍 Anaconda 指南](ANACONDA_GUIDE.md)** - conda 环境兼容
|
|
57
56
|
- **[🛠️ 可用工具](#-可用工具)** - 完整功能列表
|
|
58
57
|
- **[💡 使用示例](#-使用示例)** - 实际应用场景
|
|
59
58
|
|
|
@@ -79,12 +78,15 @@ Dynamic: license-file
|
|
|
79
78
|
- 🛡️ **完善的异常处理** - 统一的错误处理机制(7 层异常层次)
|
|
80
79
|
- 📊 **会话管理** - 支持多个并发 SSH 会话(最大 10 个,每主机 3 个)
|
|
81
80
|
- 📁 **SFTP 文件传输** - 上传、下载、目录管理
|
|
81
|
+
- 🖊️ **远程文件编辑** - 直接写入/追加文件内容,无需下载再上传
|
|
82
82
|
- 🔑 **密钥管理** - 生成和管理 SSH 密钥对(RSA/Ed25519)
|
|
83
83
|
- 📝 **审计日志** - 完整的操作审计记录(JSON 结构化日志)
|
|
84
84
|
- 🚀 **连接池** - 高性能连接复用(PooledConnection + ConnectionPool)
|
|
85
85
|
- 📊 **批量执行** - 多主机并行命令执行(BatchExecutor + AsyncBatchExecutor)
|
|
86
86
|
- 🐳 **Docker 支持** - Docker 构建和状态监控
|
|
87
|
-
- 📋 **后台任务** -
|
|
87
|
+
- 📋 **后台任务** - 可靠的后台进程启动(nohup + bash -c 包装,单次 SSH 调用无竞态)
|
|
88
|
+
- 🖥️ **screen/tmux 会话** - 持久化远程会话,SSH 断开后进程依然存活
|
|
89
|
+
- 🔍 **进程管理** - 启动/停止/查询远程进程、SSH 端口转发(tunnel)
|
|
88
90
|
- 🖥️ **CLI 命令行** - exec / upload / download / list-hosts 子命令
|
|
89
91
|
- 🔍 **看门狗** - 任务监控、心跳检测、全局异常处理
|
|
90
92
|
|
|
@@ -106,8 +108,6 @@ npx ssh-licco
|
|
|
106
108
|
pip install ssh-licco
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
> ⚠️ **Anaconda 用户注意**:如果使用 conda 环境,推荐用 `npx` 方式安装(完全隔离),或创建专用 conda 环境安装。直接在 base 环境 `pip install` 可能导致依赖冲突。详见 [Anaconda 环境指南](ANACONDA_GUIDE.md)。
|
|
110
|
-
|
|
111
111
|
### 方式三:从源码安装
|
|
112
112
|
|
|
113
113
|
```bash
|
|
@@ -277,17 +277,19 @@ npx ssh-licco
|
|
|
277
277
|
|
|
278
278
|
---
|
|
279
279
|
|
|
280
|
-
## 🛠️ 可用工具(v1.
|
|
280
|
+
## 🛠️ 可用工具(v1.3.0 扩充至 11 个)
|
|
281
281
|
|
|
282
282
|
| 工具 | 描述 | 核心能力 |
|
|
283
283
|
|------|------|---------|
|
|
284
284
|
| `ssh_connect` | 连接管理 | 自动读取环境变量/配置,支持密码+密钥认证,可保存配置,登录后自动执行命令 |
|
|
285
|
-
| `ssh_execute` | 命令执行 |
|
|
285
|
+
| `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式 |
|
|
286
286
|
| `ssh_disconnect` | 会话管理 | 断开指定会话 OR 列出所有活跃会话 |
|
|
287
|
-
| `ssh_file_transfer` | 文件传输 |
|
|
287
|
+
| `ssh_file_transfer` | 文件传输 | 上传/下载/列表/写入/追加/删除/创建目录/查看元信息(8 种操作) |
|
|
288
288
|
| `ssh_host` | 主机管理 | `action=list/add/remove` 增删查主机配置 |
|
|
289
289
|
| `ssh_docker` | Docker 管理 | `action=ps/images/build/logs` 全生命周期管理 |
|
|
290
290
|
| `ssh_generate_key` | 密钥生成 | RSA / Ed25519 密钥对 |
|
|
291
|
+
| `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
|
|
292
|
+
| `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
|
|
291
293
|
|
|
292
294
|
### 📖 详细文档
|
|
293
295
|
|
|
@@ -731,6 +733,8 @@ pytest --cov=ssh_mcp --cov-report=term-missing
|
|
|
731
733
|
|
|
732
734
|
| 版本 | 日期 | 主要变更 |
|
|
733
735
|
|------|------|----------|
|
|
736
|
+
| v1.3.3 | 2026-06-19 | 🛠️ 后台任务可靠性修复:单次 SSH 调用 + nohup bash -c 包装消除竞态;新增 screen/tmux 会话支持(session_type 参数);channel 引用保持防止进程被 GC 回收;docker build 不再被误判为后台任务;新增 ssh_session/ssh_process 工具 |
|
|
737
|
+
| v1.3.2 | 2026-06-19 | 🔧 修复 MCP_GITHUB_TOKEN 配置,Auto Release 工作流修复 |
|
|
734
738
|
| v1.2.1 | 2026-06-03 | 📝 README 文档更新,同步到 PyPI |
|
|
735
739
|
| v1.1.0 | 2026-06-01 | 🔥 精简重构!MCP 工具从 15 个合并为 7 个(ssh_connect/ssh_execute/ssh_disconnect/ssh_file_transfer/ssh_host/ssh_docker/ssh_generate_key),自动连接、智能后台检测、长任务等待 |
|
|
736
740
|
| v1.0.0 | 2026-05-31 | 🎉 首个主要版本!CLI 增强(exec/upload/download/docker-build/list-hosts)、完整测试套件(402 用例)、看门狗监控、审计日志、连接池、批量执行 |
|
|
@@ -786,4 +790,4 @@ MIT License - 详见 [LICENSE](LICENSE)
|
|
|
786
790
|
|
|
787
791
|
**Made with ❤️ by Echoqili**
|
|
788
792
|
|
|
789
|
-
*Last updated: 2026-06-
|
|
793
|
+
*Last updated: 2026-06-19*
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
|
|
21
21
|
### 核心功能
|
|
22
22
|
- **[🔐 安全配置](#-安全配置)** - 多级安全策略
|
|
23
|
-
- **[🐍 Anaconda 指南](ANACONDA_GUIDE.md)** - conda 环境兼容
|
|
24
23
|
- **[🛠️ 可用工具](#-可用工具)** - 完整功能列表
|
|
25
24
|
- **[💡 使用示例](#-使用示例)** - 实际应用场景
|
|
26
25
|
|
|
@@ -46,12 +45,15 @@
|
|
|
46
45
|
- 🛡️ **完善的异常处理** - 统一的错误处理机制(7 层异常层次)
|
|
47
46
|
- 📊 **会话管理** - 支持多个并发 SSH 会话(最大 10 个,每主机 3 个)
|
|
48
47
|
- 📁 **SFTP 文件传输** - 上传、下载、目录管理
|
|
48
|
+
- 🖊️ **远程文件编辑** - 直接写入/追加文件内容,无需下载再上传
|
|
49
49
|
- 🔑 **密钥管理** - 生成和管理 SSH 密钥对(RSA/Ed25519)
|
|
50
50
|
- 📝 **审计日志** - 完整的操作审计记录(JSON 结构化日志)
|
|
51
51
|
- 🚀 **连接池** - 高性能连接复用(PooledConnection + ConnectionPool)
|
|
52
52
|
- 📊 **批量执行** - 多主机并行命令执行(BatchExecutor + AsyncBatchExecutor)
|
|
53
53
|
- 🐳 **Docker 支持** - Docker 构建和状态监控
|
|
54
|
-
- 📋 **后台任务** -
|
|
54
|
+
- 📋 **后台任务** - 可靠的后台进程启动(nohup + bash -c 包装,单次 SSH 调用无竞态)
|
|
55
|
+
- 🖥️ **screen/tmux 会话** - 持久化远程会话,SSH 断开后进程依然存活
|
|
56
|
+
- 🔍 **进程管理** - 启动/停止/查询远程进程、SSH 端口转发(tunnel)
|
|
55
57
|
- 🖥️ **CLI 命令行** - exec / upload / download / list-hosts 子命令
|
|
56
58
|
- 🔍 **看门狗** - 任务监控、心跳检测、全局异常处理
|
|
57
59
|
|
|
@@ -73,8 +75,6 @@ npx ssh-licco
|
|
|
73
75
|
pip install ssh-licco
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
> ⚠️ **Anaconda 用户注意**:如果使用 conda 环境,推荐用 `npx` 方式安装(完全隔离),或创建专用 conda 环境安装。直接在 base 环境 `pip install` 可能导致依赖冲突。详见 [Anaconda 环境指南](ANACONDA_GUIDE.md)。
|
|
77
|
-
|
|
78
78
|
### 方式三:从源码安装
|
|
79
79
|
|
|
80
80
|
```bash
|
|
@@ -244,17 +244,19 @@ npx ssh-licco
|
|
|
244
244
|
|
|
245
245
|
---
|
|
246
246
|
|
|
247
|
-
## 🛠️ 可用工具(v1.
|
|
247
|
+
## 🛠️ 可用工具(v1.3.0 扩充至 11 个)
|
|
248
248
|
|
|
249
249
|
| 工具 | 描述 | 核心能力 |
|
|
250
250
|
|------|------|---------|
|
|
251
251
|
| `ssh_connect` | 连接管理 | 自动读取环境变量/配置,支持密码+密钥认证,可保存配置,登录后自动执行命令 |
|
|
252
|
-
| `ssh_execute` | 命令执行 |
|
|
252
|
+
| `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式 |
|
|
253
253
|
| `ssh_disconnect` | 会话管理 | 断开指定会话 OR 列出所有活跃会话 |
|
|
254
|
-
| `ssh_file_transfer` | 文件传输 |
|
|
254
|
+
| `ssh_file_transfer` | 文件传输 | 上传/下载/列表/写入/追加/删除/创建目录/查看元信息(8 种操作) |
|
|
255
255
|
| `ssh_host` | 主机管理 | `action=list/add/remove` 增删查主机配置 |
|
|
256
256
|
| `ssh_docker` | Docker 管理 | `action=ps/images/build/logs` 全生命周期管理 |
|
|
257
257
|
| `ssh_generate_key` | 密钥生成 | RSA / Ed25519 密钥对 |
|
|
258
|
+
| `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
|
|
259
|
+
| `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
|
|
258
260
|
|
|
259
261
|
### 📖 详细文档
|
|
260
262
|
|
|
@@ -698,6 +700,8 @@ pytest --cov=ssh_mcp --cov-report=term-missing
|
|
|
698
700
|
|
|
699
701
|
| 版本 | 日期 | 主要变更 |
|
|
700
702
|
|------|------|----------|
|
|
703
|
+
| v1.3.3 | 2026-06-19 | 🛠️ 后台任务可靠性修复:单次 SSH 调用 + nohup bash -c 包装消除竞态;新增 screen/tmux 会话支持(session_type 参数);channel 引用保持防止进程被 GC 回收;docker build 不再被误判为后台任务;新增 ssh_session/ssh_process 工具 |
|
|
704
|
+
| v1.3.2 | 2026-06-19 | 🔧 修复 MCP_GITHUB_TOKEN 配置,Auto Release 工作流修复 |
|
|
701
705
|
| v1.2.1 | 2026-06-03 | 📝 README 文档更新,同步到 PyPI |
|
|
702
706
|
| v1.1.0 | 2026-06-01 | 🔥 精简重构!MCP 工具从 15 个合并为 7 个(ssh_connect/ssh_execute/ssh_disconnect/ssh_file_transfer/ssh_host/ssh_docker/ssh_generate_key),自动连接、智能后台检测、长任务等待 |
|
|
703
707
|
| v1.0.0 | 2026-05-31 | 🎉 首个主要版本!CLI 增强(exec/upload/download/docker-build/list-hosts)、完整测试套件(402 用例)、看门狗监控、审计日志、连接池、批量执行 |
|
|
@@ -753,4 +757,4 @@ MIT License - 详见 [LICENSE](LICENSE)
|
|
|
753
757
|
|
|
754
758
|
**Made with ❤️ by Echoqili**
|
|
755
759
|
|
|
756
|
-
*Last updated: 2026-06-
|
|
760
|
+
*Last updated: 2026-06-19*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssh-licco
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: SSH Model Context Protocol Server - Enable SSH functionality for AI models
|
|
5
5
|
Author-email: SSH LICCO Team <contact@ssh-licco.dev>
|
|
6
6
|
License: MIT
|
|
@@ -53,7 +53,6 @@ Dynamic: license-file
|
|
|
53
53
|
|
|
54
54
|
### 核心功能
|
|
55
55
|
- **[🔐 安全配置](#-安全配置)** - 多级安全策略
|
|
56
|
-
- **[🐍 Anaconda 指南](ANACONDA_GUIDE.md)** - conda 环境兼容
|
|
57
56
|
- **[🛠️ 可用工具](#-可用工具)** - 完整功能列表
|
|
58
57
|
- **[💡 使用示例](#-使用示例)** - 实际应用场景
|
|
59
58
|
|
|
@@ -79,12 +78,15 @@ Dynamic: license-file
|
|
|
79
78
|
- 🛡️ **完善的异常处理** - 统一的错误处理机制(7 层异常层次)
|
|
80
79
|
- 📊 **会话管理** - 支持多个并发 SSH 会话(最大 10 个,每主机 3 个)
|
|
81
80
|
- 📁 **SFTP 文件传输** - 上传、下载、目录管理
|
|
81
|
+
- 🖊️ **远程文件编辑** - 直接写入/追加文件内容,无需下载再上传
|
|
82
82
|
- 🔑 **密钥管理** - 生成和管理 SSH 密钥对(RSA/Ed25519)
|
|
83
83
|
- 📝 **审计日志** - 完整的操作审计记录(JSON 结构化日志)
|
|
84
84
|
- 🚀 **连接池** - 高性能连接复用(PooledConnection + ConnectionPool)
|
|
85
85
|
- 📊 **批量执行** - 多主机并行命令执行(BatchExecutor + AsyncBatchExecutor)
|
|
86
86
|
- 🐳 **Docker 支持** - Docker 构建和状态监控
|
|
87
|
-
- 📋 **后台任务** -
|
|
87
|
+
- 📋 **后台任务** - 可靠的后台进程启动(nohup + bash -c 包装,单次 SSH 调用无竞态)
|
|
88
|
+
- 🖥️ **screen/tmux 会话** - 持久化远程会话,SSH 断开后进程依然存活
|
|
89
|
+
- 🔍 **进程管理** - 启动/停止/查询远程进程、SSH 端口转发(tunnel)
|
|
88
90
|
- 🖥️ **CLI 命令行** - exec / upload / download / list-hosts 子命令
|
|
89
91
|
- 🔍 **看门狗** - 任务监控、心跳检测、全局异常处理
|
|
90
92
|
|
|
@@ -106,8 +108,6 @@ npx ssh-licco
|
|
|
106
108
|
pip install ssh-licco
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
> ⚠️ **Anaconda 用户注意**:如果使用 conda 环境,推荐用 `npx` 方式安装(完全隔离),或创建专用 conda 环境安装。直接在 base 环境 `pip install` 可能导致依赖冲突。详见 [Anaconda 环境指南](ANACONDA_GUIDE.md)。
|
|
110
|
-
|
|
111
111
|
### 方式三:从源码安装
|
|
112
112
|
|
|
113
113
|
```bash
|
|
@@ -277,17 +277,19 @@ npx ssh-licco
|
|
|
277
277
|
|
|
278
278
|
---
|
|
279
279
|
|
|
280
|
-
## 🛠️ 可用工具(v1.
|
|
280
|
+
## 🛠️ 可用工具(v1.3.0 扩充至 11 个)
|
|
281
281
|
|
|
282
282
|
| 工具 | 描述 | 核心能力 |
|
|
283
283
|
|------|------|---------|
|
|
284
284
|
| `ssh_connect` | 连接管理 | 自动读取环境变量/配置,支持密码+密钥认证,可保存配置,登录后自动执行命令 |
|
|
285
|
-
| `ssh_execute` | 命令执行 |
|
|
285
|
+
| `ssh_execute` | 命令执行 | 自动连接、智能后台检测、长任务等待、超时控制,支持 nohup/screen/tmux 三种后台模式 |
|
|
286
286
|
| `ssh_disconnect` | 会话管理 | 断开指定会话 OR 列出所有活跃会话 |
|
|
287
|
-
| `ssh_file_transfer` | 文件传输 |
|
|
287
|
+
| `ssh_file_transfer` | 文件传输 | 上传/下载/列表/写入/追加/删除/创建目录/查看元信息(8 种操作) |
|
|
288
288
|
| `ssh_host` | 主机管理 | `action=list/add/remove` 增删查主机配置 |
|
|
289
289
|
| `ssh_docker` | Docker 管理 | `action=ps/images/build/logs` 全生命周期管理 |
|
|
290
290
|
| `ssh_generate_key` | 密钥生成 | RSA / Ed25519 密钥对 |
|
|
291
|
+
| `ssh_session` | screen/tmux 会话 | 持久化远程会话管理(create/send/capture/list/kill),SSH 断开后进程依然存活 |
|
|
292
|
+
| `ssh_process` | 进程管理 | 启动/停止/查询远程进程,SSH 端口转发(tunnel_open/tunnel_close/tunnel_list) |
|
|
291
293
|
|
|
292
294
|
### 📖 详细文档
|
|
293
295
|
|
|
@@ -731,6 +733,8 @@ pytest --cov=ssh_mcp --cov-report=term-missing
|
|
|
731
733
|
|
|
732
734
|
| 版本 | 日期 | 主要变更 |
|
|
733
735
|
|------|------|----------|
|
|
736
|
+
| v1.3.3 | 2026-06-19 | 🛠️ 后台任务可靠性修复:单次 SSH 调用 + nohup bash -c 包装消除竞态;新增 screen/tmux 会话支持(session_type 参数);channel 引用保持防止进程被 GC 回收;docker build 不再被误判为后台任务;新增 ssh_session/ssh_process 工具 |
|
|
737
|
+
| v1.3.2 | 2026-06-19 | 🔧 修复 MCP_GITHUB_TOKEN 配置,Auto Release 工作流修复 |
|
|
734
738
|
| v1.2.1 | 2026-06-03 | 📝 README 文档更新,同步到 PyPI |
|
|
735
739
|
| v1.1.0 | 2026-06-01 | 🔥 精简重构!MCP 工具从 15 个合并为 7 个(ssh_connect/ssh_execute/ssh_disconnect/ssh_file_transfer/ssh_host/ssh_docker/ssh_generate_key),自动连接、智能后台检测、长任务等待 |
|
|
736
740
|
| v1.0.0 | 2026-05-31 | 🎉 首个主要版本!CLI 增强(exec/upload/download/docker-build/list-hosts)、完整测试套件(402 用例)、看门狗监控、审计日志、连接池、批量执行 |
|
|
@@ -786,4 +790,4 @@ MIT License - 详见 [LICENSE](LICENSE)
|
|
|
786
790
|
|
|
787
791
|
**Made with ❤️ by Echoqili**
|
|
788
792
|
|
|
789
|
-
*Last updated: 2026-06-
|
|
793
|
+
*Last updated: 2026-06-19*
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
+
import shlex
|
|
6
7
|
import threading
|
|
7
8
|
import uuid
|
|
8
9
|
from importlib.metadata import version as get_version
|
|
@@ -217,7 +218,7 @@ class SSHMCPServer:
|
|
|
217
218
|
),
|
|
218
219
|
Tool(
|
|
219
220
|
name="ssh_execute",
|
|
220
|
-
description="Execute a command on a remote server. If session_id is omitted, auto-connects using environment variables. Supports background execution for long-running tasks (auto-detected or manual). Use
|
|
221
|
+
description="Execute a command on a remote server. If session_id is omitted, auto-connects using environment variables. Supports background execution for long-running tasks (auto-detected or manual). Use session_type='screen' or 'tmux' for persistent sessions that survive SSH disconnect.",
|
|
221
222
|
inputSchema={
|
|
222
223
|
"type": "object",
|
|
223
224
|
"properties": {
|
|
@@ -228,7 +229,8 @@ class SSHMCPServer:
|
|
|
228
229
|
"workdir": {"type": "string", "description": "Working directory for background tasks.", "default": "/tmp"},
|
|
229
230
|
"log_file": {"type": "string", "description": "Log file path for background task output.", "default": "/tmp/background_task.log"},
|
|
230
231
|
"wait": {"type": "boolean", "description": "Wait for background task to complete.", "default": False},
|
|
231
|
-
"wait_timeout": {"type": "integer", "description": "Max wait time in seconds when wait=True.", "default": 60}
|
|
232
|
+
"wait_timeout": {"type": "integer", "description": "Max wait time in seconds when wait=True.", "default": 60},
|
|
233
|
+
"session_type": {"type": "string", "enum": ["nohup", "screen", "tmux"], "default": "nohup", "description": "Background session type: nohup (default), screen, or tmux (persistent)."}
|
|
232
234
|
},
|
|
233
235
|
"required": ["command"]
|
|
234
236
|
}
|
|
@@ -571,26 +573,29 @@ Current security level: {os.getenv('SSH_SECURITY_LEVEL', 'balanced')}"""
|
|
|
571
573
|
return [TextContent(type="text", text=output)]
|
|
572
574
|
|
|
573
575
|
async def _execute_background(self, session_id: str, command: str, args: dict, timeout: int) -> list[TextContent]:
|
|
574
|
-
"""Execute a command as a background task"""
|
|
576
|
+
"""Execute a command as a background task with proper nohup + bash -c wrapping"""
|
|
575
577
|
from .security import SecurityError, command_validator, path_validator
|
|
576
578
|
|
|
577
579
|
workdir = args.get("workdir", "/tmp")
|
|
578
580
|
log_file = args.get("log_file", "/tmp/background_task.log")
|
|
579
581
|
wait = args.get("wait", False)
|
|
580
582
|
wait_timeout = args.get("wait_timeout", 60)
|
|
583
|
+
session_type = args.get("session_type", "nohup") # "nohup" | "screen" | "tmux"
|
|
581
584
|
|
|
585
|
+
# Validate the base command
|
|
582
586
|
try:
|
|
583
587
|
command_validator.validate_command(command.split()[0] if command.split() else "")
|
|
584
588
|
except SecurityError as e:
|
|
585
589
|
return [TextContent(type="text", text=f"Security error: {str(e)}")]
|
|
586
590
|
|
|
587
|
-
# workdir/log_file
|
|
591
|
+
# workdir/log_file are remote paths, don't use local path_validator
|
|
588
592
|
try:
|
|
589
593
|
safe_workdir = self._sanitize_remote_path(workdir)
|
|
590
594
|
safe_log_file = self._sanitize_remote_path(log_file)
|
|
591
595
|
except SecurityError as e:
|
|
592
596
|
return [TextContent(type="text", text=f"Path not allowed: {str(e)}")]
|
|
593
597
|
|
|
598
|
+
# Block dangerous patterns
|
|
594
599
|
dangerous_patterns = ['rm -rf /', 'mkfs', 'dd if=/dev/zero', ':(){:|:&};:', 'chmod -R 777 /']
|
|
595
600
|
for pattern in dangerous_patterns:
|
|
596
601
|
if pattern in command:
|
|
@@ -599,78 +604,79 @@ Current security level: {os.getenv('SSH_SECURITY_LEVEL', 'balanced')}"""
|
|
|
599
604
|
task_id = str(uuid.uuid4())[:8]
|
|
600
605
|
pid_file = f"/tmp/task_{task_id}.pid"
|
|
601
606
|
|
|
602
|
-
#
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
607
|
+
# ── screen / tmux session support ──
|
|
608
|
+
if session_type in ("screen", "tmux"):
|
|
609
|
+
return await self._execute_background_session(
|
|
610
|
+
session_id, command, safe_workdir, safe_log_file,
|
|
611
|
+
session_type, task_id, wait, wait_timeout
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# ── nohup mode (default): single SSH call, no race condition ──
|
|
615
|
+
# Use shlex.quote() to safely escape the command and paths for bash -c.
|
|
616
|
+
# The wrapper: cd → nohup bash -c '...' → capture PID → sleep → check alive.
|
|
617
|
+
check_cmd = (
|
|
618
|
+
f"cd {shlex.quote(safe_workdir)} && "
|
|
619
|
+
f"nohup bash -c {shlex.quote(command)} "
|
|
620
|
+
f"> {shlex.quote(safe_log_file)} 2>&1 < /dev/null & "
|
|
621
|
+
f"PID=$! && "
|
|
622
|
+
f"echo $PID > {shlex.quote(pid_file)} && "
|
|
623
|
+
f"sleep 1 && "
|
|
624
|
+
f"if ps -p $PID > /dev/null 2>&1; then "
|
|
625
|
+
f"echo 'PID='$PID' STATUS=RUNNING'; "
|
|
626
|
+
f"else "
|
|
627
|
+
f"echo 'PID='$PID' STATUS=DEAD'; "
|
|
628
|
+
f"echo '--- LOG ---'; "
|
|
629
|
+
f"cat {shlex.quote(safe_log_file)} 2>/dev/null || echo '(no output)'; "
|
|
630
|
+
f"fi"
|
|
611
631
|
)
|
|
612
632
|
|
|
613
633
|
try:
|
|
614
|
-
#
|
|
615
|
-
|
|
616
|
-
|
|
634
|
+
# Execute as a single command (background=False) — the wrapper
|
|
635
|
+
# includes sleep + status check, so it returns in ~1 second.
|
|
636
|
+
result = await self.session_manager.execute_command(
|
|
637
|
+
session_id, check_cmd, timeout=timeout + 5, background=False
|
|
617
638
|
)
|
|
639
|
+
except Exception as e:
|
|
640
|
+
return [TextContent(type="text", text=f"Error starting background task: {str(e)}")]
|
|
618
641
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
642
|
+
start_stdout = (result.get("stdout") or "").strip()
|
|
643
|
+
start_stderr = (result.get("stderr") or "").strip()
|
|
644
|
+
|
|
645
|
+
# Parse output: PID=xxx STATUS=RUNNING|DEAD
|
|
646
|
+
pid = ""
|
|
647
|
+
status = ""
|
|
648
|
+
log_tail = ""
|
|
649
|
+
in_log = False
|
|
650
|
+
for line in start_stdout.splitlines():
|
|
651
|
+
if line.startswith("PID="):
|
|
652
|
+
parts = line.split(None, 1)
|
|
653
|
+
pid = parts[0].split("=", 1)[1].strip() if len(parts) >= 1 else ""
|
|
654
|
+
if len(parts) >= 2 and parts[1].startswith("STATUS="):
|
|
655
|
+
status = parts[1].split("=", 1)[1].strip()
|
|
656
|
+
elif line == "--- LOG ---":
|
|
657
|
+
in_log = True
|
|
658
|
+
elif in_log:
|
|
659
|
+
log_tail += line + "\n"
|
|
660
|
+
|
|
661
|
+
# Startup failed
|
|
662
|
+
if status == "DEAD" or not pid:
|
|
663
|
+
return [TextContent(type="text", text=(
|
|
664
|
+
f"Background task failed to start!\n\n"
|
|
665
|
+
f"Command: {command}\n"
|
|
666
|
+
f"PID: {pid or '(none)'}\n"
|
|
667
|
+
f"Workdir: {safe_workdir}\n"
|
|
668
|
+
f"--- STDOUT ---\n{start_stdout}\n"
|
|
669
|
+
f"--- STDERR ---\n{start_stderr}\n"
|
|
670
|
+
f"--- LOG TAIL ---\n{log_tail or '(no log output)'}"
|
|
671
|
+
))]
|
|
672
|
+
|
|
673
|
+
if wait:
|
|
674
|
+
output = await self._wait_for_task_completion(
|
|
675
|
+
session_id=session_id, task_id=task_id,
|
|
676
|
+
log_file=safe_log_file, timeout=wait_timeout
|
|
637
677
|
)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
# 解析远程进程真实 PID 和状态
|
|
642
|
-
pid = ""
|
|
643
|
-
status = ""
|
|
644
|
-
log_tail = ""
|
|
645
|
-
in_log = False
|
|
646
|
-
for line in start_stdout.splitlines():
|
|
647
|
-
if line.startswith("PID="):
|
|
648
|
-
pid = line.split("=", 1)[1].strip()
|
|
649
|
-
elif line.startswith("STATUS="):
|
|
650
|
-
status = line.split("=", 1)[1].strip()
|
|
651
|
-
elif line == "--- LOG ---":
|
|
652
|
-
in_log = True
|
|
653
|
-
elif in_log:
|
|
654
|
-
log_tail += line + "\n"
|
|
655
|
-
|
|
656
|
-
# 启动失败:进程已死或没拿到 PID
|
|
657
|
-
if status == "DEAD" or not pid:
|
|
658
|
-
return [TextContent(type="text", text=(
|
|
659
|
-
f"Background task failed to start!\n\n"
|
|
660
|
-
f"Command: {command}\n"
|
|
661
|
-
f"PID: {pid or '(none)'}\n"
|
|
662
|
-
f"--- STDOUT ---\n{start_stdout}\n"
|
|
663
|
-
f"--- STDERR ---\n{start_stderr}\n"
|
|
664
|
-
f"--- LOG TAIL ---\n{log_tail}"
|
|
665
|
-
))]
|
|
666
|
-
|
|
667
|
-
if wait:
|
|
668
|
-
output = await self._wait_for_task_completion(
|
|
669
|
-
session_id=session_id, task_id=task_id,
|
|
670
|
-
log_file=safe_log_file, timeout=wait_timeout
|
|
671
|
-
)
|
|
672
|
-
else:
|
|
673
|
-
output = f"""Background Task Started!
|
|
678
|
+
else:
|
|
679
|
+
output = f"""Background Task Started!
|
|
674
680
|
|
|
675
681
|
Task ID: {task_id}
|
|
676
682
|
PID: {pid}
|
|
@@ -687,9 +693,70 @@ To check if still running:
|
|
|
687
693
|
ssh_execute(session_id="{session_id}", command="ps -p {pid}")
|
|
688
694
|
"""
|
|
689
695
|
|
|
690
|
-
|
|
696
|
+
return [TextContent(type="text", text=output)]
|
|
697
|
+
|
|
698
|
+
async def _execute_background_session(
|
|
699
|
+
self, session_id: str, command: str, workdir: str,
|
|
700
|
+
log_file: str, session_type: str, task_id: str,
|
|
701
|
+
wait: bool, wait_timeout: int
|
|
702
|
+
) -> list[TextContent]:
|
|
703
|
+
"""Execute a command inside a screen/tmux session for persistent long-running tasks"""
|
|
704
|
+
session_name = f"ssh_mcp_{task_id}"
|
|
705
|
+
escaped_cmd = command.replace("'", "'\\''")
|
|
706
|
+
|
|
707
|
+
if session_type == "screen":
|
|
708
|
+
# screen -dmS <name> bash -c 'cd <dir> && <cmd> > <log> 2>&1'
|
|
709
|
+
launch_cmd = (
|
|
710
|
+
f"screen -dmS {shlex.quote(session_name)} bash -c "
|
|
711
|
+
f"'cd {shlex.quote(workdir)} && {escaped_cmd} > {shlex.quote(log_file)} 2>&1'"
|
|
712
|
+
)
|
|
713
|
+
else: # tmux
|
|
714
|
+
launch_cmd = (
|
|
715
|
+
f"tmux new-session -d -s {shlex.quote(session_name)} "
|
|
716
|
+
f"'cd {shlex.quote(workdir)} && {escaped_cmd} > {shlex.quote(log_file)} 2>&1'"
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
result = await self.session_manager.execute_command(
|
|
721
|
+
session_id, launch_cmd, timeout=10, background=False
|
|
722
|
+
)
|
|
691
723
|
except Exception as e:
|
|
692
|
-
return [TextContent(type="text", text=f"Error starting
|
|
724
|
+
return [TextContent(type="text", text=f"Error starting {session_type} session: {str(e)}")]
|
|
725
|
+
|
|
726
|
+
stderr = (result.get("stderr") or "").strip()
|
|
727
|
+
if stderr:
|
|
728
|
+
return [TextContent(type="text", text=(
|
|
729
|
+
f"Failed to start {session_type} session!\n\n"
|
|
730
|
+
f"Session: {session_name}\n"
|
|
731
|
+
f"Command: {command}\n"
|
|
732
|
+
f"Error: {stderr}"
|
|
733
|
+
))]
|
|
734
|
+
|
|
735
|
+
if wait:
|
|
736
|
+
output = await self._wait_for_task_completion(
|
|
737
|
+
session_id=session_id, task_id=task_id,
|
|
738
|
+
log_file=log_file, timeout=wait_timeout
|
|
739
|
+
)
|
|
740
|
+
else:
|
|
741
|
+
attach_cmd = f"screen -r {session_name}" if session_type == "screen" else f"tmux attach -t {session_name}"
|
|
742
|
+
output = f"""{session_type.upper()} Session Started!
|
|
743
|
+
|
|
744
|
+
Task ID: {task_id}
|
|
745
|
+
Session Name: {session_name}
|
|
746
|
+
Command: {command}
|
|
747
|
+
Working Directory: {workdir}
|
|
748
|
+
Log File: {log_file}
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
To attach to session:
|
|
752
|
+
ssh_execute(session_id="{session_id}", command="{attach_cmd}")
|
|
753
|
+
To view log:
|
|
754
|
+
ssh_execute(session_id="{session_id}", command="cat {log_file}")
|
|
755
|
+
To kill session:
|
|
756
|
+
ssh_execute(session_id="{session_id}", command="{'screen -XS ' + session_name + ' quit' if session_type == 'screen' else 'tmux kill-session -t ' + session_name}")
|
|
757
|
+
"""
|
|
758
|
+
|
|
759
|
+
return [TextContent(type="text", text=output)]
|
|
693
760
|
|
|
694
761
|
async def _wait_for_task_completion(self, session_id: str, task_id: str, log_file: str, timeout: int) -> str:
|
|
695
762
|
pid_file = f"/tmp/task_{task_id}.pid"
|
|
@@ -742,6 +809,8 @@ Use ssh_execute to check: cat {log_file}
|
|
|
742
809
|
'docker pull', 'docker push', 'docker save', 'docker load',
|
|
743
810
|
'docker network ls', 'docker volume ls', 'docker system',
|
|
744
811
|
'docker exec', 'docker attach', 'docker cp',
|
|
812
|
+
'docker build', 'docker buildx', 'docker compose build',
|
|
813
|
+
'docker-compose build', 'docker tag', 'docker login', 'docker logout',
|
|
745
814
|
]
|
|
746
815
|
for cmd in docker_instant_commands:
|
|
747
816
|
if cmd in command_lower:
|
|
@@ -54,6 +54,7 @@ class SSHSession:
|
|
|
54
54
|
self._executor = get_executor()
|
|
55
55
|
self._connect_lock = asyncio.Lock()
|
|
56
56
|
self._shutdown_event = asyncio.Event()
|
|
57
|
+
self._background_channels: list = [] # 保持 background channel 引用,防止 GC 关闭 channel
|
|
57
58
|
|
|
58
59
|
@property
|
|
59
60
|
def state(self) -> SessionState:
|
|
@@ -176,6 +177,14 @@ class SSHSession:
|
|
|
176
177
|
stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
|
|
177
178
|
|
|
178
179
|
if background:
|
|
180
|
+
# Keep channel references alive to prevent GC from closing the
|
|
181
|
+
# channel while the background process is still starting up.
|
|
182
|
+
# The channel will be cleaned up when the session disconnects.
|
|
183
|
+
self._background_channels.append((stdin, stdout, stderr))
|
|
184
|
+
# Limit stored channels to prevent memory leak
|
|
185
|
+
if len(self._background_channels) > 10:
|
|
186
|
+
self._background_channels = self._background_channels[-5:]
|
|
187
|
+
|
|
179
188
|
try:
|
|
180
189
|
pid = stdout.channel.pid # type: ignore[attr-defined]
|
|
181
190
|
pid_msg = f"PID: {pid}"
|
|
@@ -491,6 +500,13 @@ class SSHSession:
|
|
|
491
500
|
if self._keepalive_task:
|
|
492
501
|
self._keepalive_task.cancel()
|
|
493
502
|
self._keepalive_task = None
|
|
503
|
+
# Close any lingering background channels
|
|
504
|
+
for stdin, stdout, stderr in self._background_channels:
|
|
505
|
+
try:
|
|
506
|
+
stdout.channel.close()
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
self._background_channels.clear()
|
|
494
510
|
if self.client:
|
|
495
511
|
await self._executor.submit(self.client.close)
|
|
496
512
|
self.client = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|