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.
Files changed (34) hide show
  1. {ssh_licco-1.3.2/ssh_licco.egg-info → ssh_licco-1.3.3}/PKG-INFO +13 -9
  2. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/README.md +12 -8
  3. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/pyproject.toml +1 -1
  4. {ssh_licco-1.3.2 → ssh_licco-1.3.3/ssh_licco.egg-info}/PKG-INFO +13 -9
  5. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/__init__.py +2 -2
  6. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/server.py +141 -72
  7. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/session_manager.py +16 -0
  8. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/LICENSE +0 -0
  9. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/setup.cfg +0 -0
  10. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/SOURCES.txt +0 -0
  11. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/dependency_links.txt +0 -0
  12. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/entry_points.txt +0 -0
  13. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/requires.txt +0 -0
  14. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_licco.egg-info/top_level.txt +0 -0
  15. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/audit_logger.py +0 -0
  16. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/batch_executor.py +0 -0
  17. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/cli.py +0 -0
  18. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/__init__.py +0 -0
  19. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/additional_clients.py +0 -0
  20. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/factory.py +0 -0
  21. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/interface.py +0 -0
  22. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/clients/paramiko_client.py +0 -0
  23. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/config_manager.py +0 -0
  24. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/connection_config.py +0 -0
  25. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/connection_pool.py +0 -0
  26. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/exceptions.py +0 -0
  27. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/executor.py +0 -0
  28. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/fallback_executor.py +0 -0
  29. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/key_manager.py +0 -0
  30. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/logging_config.py +0 -0
  31. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/security.py +0 -0
  32. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/service.py +0 -0
  33. {ssh_licco-1.3.2 → ssh_licco-1.3.3}/ssh_mcp/watchdog.py +0 -0
  34. {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.2
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.1.0 精简至 7 个)
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-01*
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.1.0 精简至 7 个)
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-01*
760
+ *Last updated: 2026-06-19*
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssh-licco"
3
- version = "1.3.2"
3
+ version = "1.3.3"
4
4
  description = "SSH Model Context Protocol Server - Enable SSH functionality for AI models"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-licco
3
- Version: 1.3.2
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.1.0 精简至 7 个)
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-01*
793
+ *Last updated: 2026-06-19*
@@ -1,8 +1,8 @@
1
1
  # SSH-LICCO 版本信息
2
2
  # 所有代码中的版本信息都从这里读取
3
- # 版本号:1.3.2
3
+ # 版本号:1.3.3
4
4
 
5
- __version__ = "1.3.2"
5
+ __version__ = "1.3.3"
6
6
  __author__ = "Li Qi"
7
7
  __email__ = "1985694657@qq.com"
8
8
  __license__ = "MIT"
@@ -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 background=True for web servers, Docker builds, compilations, etc.",
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 是远程路径,不能用本地 path_validator(会把 /tmp 解析成 D:\tmp)
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
- # setsid: 新建会话,脱离 SSH 控制终端; nohup: 忽略 SIGHUP;
603
- # < /dev/null: 不占用 channel stdin(否则 channel 无法干净关闭);
604
- # &: 后台; disown: 移出 shell job 表
605
- # 注意: 必须用 background=True 执行,否则 paramiko 的 chan.makefile().read()
606
- # 会因后台进程 & 持有 stdout 而永远等待 EOF,导致 channel 挂起。
607
- background_command = (
608
- f"cd {safe_workdir} && "
609
- f"setsid nohup {command} > {safe_log_file} 2>&1 < /dev/null & "
610
- f"echo $! > {pid_file}; disown 2>/dev/null || true"
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
- # Step 1: background=True 启动包装命令(立即返回,不挂起)
615
- await self.session_manager.execute_command(
616
- session_id, background_command, timeout=10, background=True
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
- # Step 2: 等待 PID 文件写入
620
- await asyncio.sleep(0.5)
621
-
622
- # Step 3: 单独读取 PID 文件并检查进程是否存活
623
- # 如果进程已死(命令拼写错误、权限不足等),读取日志显示启动错误
624
- read_cmd = (
625
- f"PID=$(cat {pid_file} 2>/dev/null); "
626
- f"echo \"PID=$PID\"; "
627
- f"if [ -n \"$PID\" ] && ps -p $PID > /dev/null 2>&1; then "
628
- f"echo 'STATUS=RUNNING'; "
629
- f"else "
630
- f"echo 'STATUS=DEAD'; "
631
- f"echo '--- LOG ---'; "
632
- f"cat {safe_log_file} 2>&1 | tail -20; "
633
- f"fi"
634
- )
635
- start_result = await self.session_manager.execute_command(
636
- session_id, read_cmd, timeout=10
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
- start_stdout = (start_result.get("stdout") or "").strip()
639
- start_stderr = (start_result.get("stderr") or "").strip()
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
- return [TextContent(type="text", text=output)]
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 background task: {str(e)}")]
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