ssh-licco 2.1.1__tar.gz → 2.1.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 (56) hide show
  1. {ssh_licco-2.1.1/ssh_licco.egg-info → ssh_licco-2.1.3}/PKG-INFO +2 -2
  2. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/README.md +1 -1
  3. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/pyproject.toml +1 -1
  4. {ssh_licco-2.1.1 → ssh_licco-2.1.3/ssh_licco.egg-info}/PKG-INFO +2 -2
  5. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_licco.egg-info/SOURCES.txt +1 -0
  6. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/__init__.py +2 -2
  7. ssh_licco-2.1.3/ssh_mcp/cli.py +135 -0
  8. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/file_transfer.py +56 -0
  9. ssh_licco-2.1.3/ssh_mcp/server.py +1918 -0
  10. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/session_manager.py +13 -0
  11. ssh_licco-2.1.1/ssh_mcp/server.py +0 -172
  12. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/LICENSE +0 -0
  13. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/setup.cfg +0 -0
  14. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_licco.egg-info/dependency_links.txt +0 -0
  15. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_licco.egg-info/entry_points.txt +0 -0
  16. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_licco.egg-info/requires.txt +0 -0
  17. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_licco.egg-info/top_level.txt +0 -0
  18. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/__main__.py +0 -0
  19. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/approval.py +0 -0
  20. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/audit_logger.py +0 -0
  21. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/batch_executor.py +0 -0
  22. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/clients/__init__.py +0 -0
  23. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/clients/additional_clients.py +0 -0
  24. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/clients/factory.py +0 -0
  25. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/clients/interface.py +0 -0
  26. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/clients/paramiko_client.py +0 -0
  27. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/config_manager.py +0 -0
  28. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/connection_config.py +0 -0
  29. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/connection_pool.py +0 -0
  30. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/exceptions.py +0 -0
  31. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/executor.py +0 -0
  32. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/__init__.py +0 -0
  33. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/approval.py +0 -0
  34. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/connect.py +0 -0
  35. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/context.py +0 -0
  36. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/docker.py +0 -0
  37. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/execute.py +0 -0
  38. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/host.py +0 -0
  39. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/key.py +0 -0
  40. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/process.py +0 -0
  41. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/schemas.py +0 -0
  42. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/session.py +0 -0
  43. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/handlers/utils.py +0 -0
  44. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/key_manager.py +0 -0
  45. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/logging_config.py +0 -0
  46. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/runtime_guard.py +0 -0
  47. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/secret_provider.py +0 -0
  48. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/security.py +0 -0
  49. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/service.py +0 -0
  50. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/tunnel.py +0 -0
  51. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/ssh_mcp/watchdog.py +0 -0
  52. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/tests/test_config_manager.py +0 -0
  53. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/tests/test_key_manager.py +0 -0
  54. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/tests/test_security.py +0 -0
  55. {ssh_licco-2.1.1 → ssh_licco-2.1.3}/tests/test_server.py +0 -0
  56. {ssh_licco-2.1.1 → ssh_licco-2.1.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: 2.1.1
3
+ Version: 2.1.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
@@ -575,7 +575,7 @@ ssh-licco 的核心依赖是:
575
575
 
576
576
  | 指标 | 状态 |
577
577
  |------|------|
578
- | **测试用例** | 344 passed, 0 skipped |
578
+ | **测试用例** | 348 passed, 0 skipped |
579
579
  | **覆盖率** | 16 个源模块全覆盖 |
580
580
  | **测试框架** | pytest + pytest-asyncio |
581
581
 
@@ -542,7 +542,7 @@ ssh-licco 的核心依赖是:
542
542
 
543
543
  | 指标 | 状态 |
544
544
  |------|------|
545
- | **测试用例** | 344 passed, 0 skipped |
545
+ | **测试用例** | 348 passed, 0 skipped |
546
546
  | **覆盖率** | 16 个源模块全覆盖 |
547
547
  | **测试框架** | pytest + pytest-asyncio |
548
548
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssh-licco"
3
- version = "2.1.1"
3
+ version = "2.1.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: 2.1.1
3
+ Version: 2.1.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
@@ -575,7 +575,7 @@ ssh-licco 的核心依赖是:
575
575
 
576
576
  | 指标 | 状态 |
577
577
  |------|------|
578
- | **测试用例** | 344 passed, 0 skipped |
578
+ | **测试用例** | 348 passed, 0 skipped |
579
579
  | **覆盖率** | 16 个源模块全覆盖 |
580
580
  | **测试框架** | pytest + pytest-asyncio |
581
581
 
@@ -12,6 +12,7 @@ ssh_mcp/__main__.py
12
12
  ssh_mcp/approval.py
13
13
  ssh_mcp/audit_logger.py
14
14
  ssh_mcp/batch_executor.py
15
+ ssh_mcp/cli.py
15
16
  ssh_mcp/config_manager.py
16
17
  ssh_mcp/connection_config.py
17
18
  ssh_mcp/connection_pool.py
@@ -1,8 +1,8 @@
1
1
  # SSH-LICCO 版本信息
2
2
  # 所有代码中的版本信息都从这里读取
3
- # 版本号:2.1.1
3
+ # 版本号:2.1.3
4
4
 
5
- __version__ = "2.1.1"
5
+ __version__ = "2.1.3"
6
6
  __author__ = "Li Qi"
7
7
  __email__ = "1985694657@qq.com"
8
8
  __license__ = "MIT"
@@ -0,0 +1,135 @@
1
+ """SSH-LICCO command line interface."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from ssh_mcp import __version__
10
+
11
+
12
+ def _build_parser() -> argparse.ArgumentParser:
13
+ """Build the CLI argument parser."""
14
+ parser = argparse.ArgumentParser(
15
+ prog="ssh-licco",
16
+ description="SSH-LICCO: Remote server management via MCP and CLI.",
17
+ )
18
+ parser.add_argument(
19
+ "--version",
20
+ action="version",
21
+ version=f"ssh-licco {__version__}",
22
+ )
23
+
24
+ subparsers = parser.add_subparsers(dest="subcommand", required=False)
25
+
26
+ # exec
27
+ exec_parser = subparsers.add_parser("exec", help="Execute a remote command")
28
+ exec_parser.add_argument("cmd", help="Command to execute")
29
+ exec_parser.add_argument("--host", default=None, help="Remote host")
30
+ exec_parser.add_argument("-u", "--username", default=None, help="SSH username")
31
+ exec_parser.add_argument("--password", default=None, help="SSH password")
32
+ exec_parser.add_argument(
33
+ "--connect-timeout", type=int, default=None, help="Connection timeout in seconds"
34
+ )
35
+
36
+ # upload
37
+ upload_parser = subparsers.add_parser("upload", help="Upload a file")
38
+ upload_parser.add_argument("local", help="Local file path")
39
+ upload_parser.add_argument("remote", help="Remote file path")
40
+ upload_parser.add_argument("--host", default=None, help="Remote host")
41
+ upload_parser.add_argument("-u", "--username", default=None, help="SSH username")
42
+ upload_parser.add_argument("--password", default=None, help="SSH password")
43
+
44
+ # download
45
+ download_parser = subparsers.add_parser("download", help="Download a file")
46
+ download_parser.add_argument("remote", help="Remote file path")
47
+ download_parser.add_argument("local", help="Local file path")
48
+ download_parser.add_argument("--host", default=None, help="Remote host")
49
+ download_parser.add_argument("-u", "--username", default=None, help="SSH username")
50
+ download_parser.add_argument("--password", default=None, help="SSH password")
51
+
52
+ # docker-build
53
+ docker_build_parser = subparsers.add_parser("docker-build", help="Build a Docker image")
54
+ docker_build_parser.add_argument("image", help="Image tag")
55
+ docker_build_parser.add_argument("-c", "--context", default=".", help="Build context")
56
+ docker_build_parser.add_argument("-f", "--dockerfile", default="Dockerfile", help="Dockerfile path")
57
+ docker_build_parser.add_argument("-t", "--timeout", type=int, default=600, help="Build timeout")
58
+
59
+ # serve
60
+ subparsers.add_parser("serve", help="Start the MCP server")
61
+
62
+ # list-hosts
63
+ subparsers.add_parser("list-hosts", help="List configured hosts")
64
+
65
+ return parser
66
+
67
+
68
+ def _load_connection_config(args: argparse.Namespace) -> dict:
69
+ """Load connection config from CLI args or environment variables."""
70
+ host = args.host or os.getenv("SSH_HOST")
71
+ username = args.username or os.getenv("SSH_USER")
72
+ password = args.password or os.getenv("SSH_PASSWORD")
73
+ port = args.port if getattr(args, "port", None) else 22
74
+ timeout = args.connect_timeout if getattr(args, "connect_timeout", None) else 30
75
+
76
+ return {
77
+ "host": host,
78
+ "port": port,
79
+ "username": username,
80
+ "password": password,
81
+ "timeout": timeout,
82
+ }
83
+
84
+
85
+ def main(argv: list[str] | None = None) -> int:
86
+ """CLI entry point."""
87
+ parser = _build_parser()
88
+ args = parser.parse_args(argv)
89
+
90
+ if args.subcommand is None:
91
+ parser.print_help()
92
+ return 0
93
+
94
+ if args.subcommand == "serve":
95
+ from ssh_mcp.server import run_server
96
+
97
+ run_server()
98
+ return 0
99
+
100
+ if args.subcommand == "list-hosts":
101
+ from ssh_mcp.config_manager import ConfigManager
102
+
103
+ manager = ConfigManager()
104
+ hosts = manager.list_hosts()
105
+ if not hosts:
106
+ print("No hosts configured.")
107
+ return 0
108
+ for name, config in hosts.items():
109
+ print(f"{name}: {config.host}:{config.port} ({config.username})")
110
+ return 0
111
+
112
+ if args.subcommand == "exec":
113
+ config = _load_connection_config(args)
114
+ print(f"Would execute on {config['host']}: {args.cmd}")
115
+ return 0
116
+
117
+ if args.subcommand == "upload":
118
+ config = _load_connection_config(args)
119
+ print(f"Would upload {args.local} -> {config['host']}:{args.remote}")
120
+ return 0
121
+
122
+ if args.subcommand == "download":
123
+ config = _load_connection_config(args)
124
+ print(f"Would download {config['host']}:{args.remote} -> {args.local}")
125
+ return 0
126
+
127
+ if args.subcommand == "docker-build":
128
+ print(f"Would build Docker image {args.image} from {args.context}/{args.dockerfile}")
129
+ return 0
130
+
131
+ return 0
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
  import shlex
7
+ from pathlib import PurePosixPath
7
8
 
8
9
  from mcp.types import TextContent
9
10
 
@@ -12,6 +13,58 @@ from .context import HandlerContext
12
13
  from .utils import ensure_session
13
14
 
14
15
 
16
+ # 远程敏感路径黑名单:禁止通过文件传输操作访问或删除
17
+ FORBIDDEN_REMOTE_PATHS = [
18
+ "/etc",
19
+ "/root",
20
+ "/boot",
21
+ "/proc",
22
+ "/sys",
23
+ "/dev",
24
+ "/bin",
25
+ "/sbin",
26
+ "/usr/bin",
27
+ "/usr/sbin",
28
+ "/lib",
29
+ "/lib64",
30
+ "/var/lib/postgresql",
31
+ "/var/lib/mysql",
32
+ "/var/lib/redis",
33
+ "/var/lib/docker",
34
+ "/var/log",
35
+ "/var/spool",
36
+ "/var/mail",
37
+ "/home/*/.ssh",
38
+ ]
39
+
40
+
41
+ def _validate_remote_path(remote_path: str, operation: str = "access") -> tuple[bool, str]:
42
+ """校验远程路径是否安全。
43
+
44
+ Returns:
45
+ (is_safe, error_message)
46
+ """
47
+ if not remote_path or not remote_path.strip():
48
+ return False, "远程路径不能为空"
49
+
50
+ # 路径遍历检查
51
+ normalized = str(PurePosixPath(remote_path)).rstrip("/") or "/"
52
+ if ".." in normalized.split("/"):
53
+ return False, f"路径遍历被阻止:{remote_path}"
54
+
55
+ # 检查是否命中敏感路径黑名单
56
+ path_for_check = normalized.rstrip("/*") or "/"
57
+ for forbidden in FORBIDDEN_REMOTE_PATHS:
58
+ if forbidden.endswith("/*"):
59
+ prefix = forbidden[:-1]
60
+ if path_for_check.startswith(prefix) or path_for_check + "/" == prefix:
61
+ return False, f"禁止{operation}敏感路径:{forbidden}"
62
+ elif path_for_check == forbidden or path_for_check.startswith(forbidden + "/"):
63
+ return False, f"禁止{operation}敏感路径:{forbidden}"
64
+
65
+ return True, ""
66
+
67
+
15
68
  async def handle_file_transfer(ctx: HandlerContext, args: dict) -> list[TextContent]:
16
69
  """Transfer files between local and remote, or perform remote file operations."""
17
70
  session_id = await ensure_session(ctx, args, handle_connect)
@@ -51,6 +104,9 @@ async def handle_file_transfer(ctx: HandlerContext, args: dict) -> list[TextCont
51
104
  elif direction == "delete":
52
105
  if not remote_path:
53
106
  return [TextContent(type="text", text="delete requires remote_path")]
107
+ safe, err = _validate_remote_path(remote_path, operation="delete")
108
+ if not safe:
109
+ return [TextContent(type="text", text=f"❌ Delete blocked: {err}")]
54
110
  result = await session.delete_file(remote_path)
55
111
  elif direction == "mkdir":
56
112
  if not remote_path: