tunnel-manager 1.0.9__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.
- scripts/validate_a2a_agent.py +148 -0
- scripts/validate_agent.py +67 -0
- tests/test_tunnel.py +76 -0
- tunnel_manager/__init__.py +66 -0
- tunnel_manager/__main__.py +6 -0
- tunnel_manager/mcp_config.json +8 -0
- tunnel_manager/middlewares.py +53 -0
- tunnel_manager/skills/tunnel-manager-remote-access/SKILL.md +51 -0
- tunnel_manager/tunnel_manager.py +990 -0
- tunnel_manager/tunnel_manager_agent.py +350 -0
- tunnel_manager/tunnel_manager_mcp.py +2600 -0
- tunnel_manager/utils.py +110 -0
- tunnel_manager-1.0.9.dist-info/METADATA +565 -0
- tunnel_manager-1.0.9.dist-info/RECORD +18 -0
- tunnel_manager-1.0.9.dist-info/WHEEL +5 -0
- tunnel_manager-1.0.9.dist-info/entry_points.txt +4 -0
- tunnel_manager-1.0.9.dist-info/licenses/LICENSE +20 -0
- tunnel_manager-1.0.9.dist-info/top_level.txt +3 -0
tunnel_manager/utils.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import pickle
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Union, List
|
|
8
|
+
from importlib.resources import files, as_file
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
from fasta2a import Skill
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def to_integer(string: Union[str, int] = None) -> int:
|
|
15
|
+
if isinstance(string, int):
|
|
16
|
+
return string
|
|
17
|
+
if not string:
|
|
18
|
+
return 0
|
|
19
|
+
try:
|
|
20
|
+
return int(string.strip())
|
|
21
|
+
except ValueError:
|
|
22
|
+
raise ValueError(f"Cannot convert '{string}' to integer")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def to_boolean(string: Union[str, bool] = None) -> bool:
|
|
26
|
+
if isinstance(string, bool):
|
|
27
|
+
return string
|
|
28
|
+
if not string:
|
|
29
|
+
return False
|
|
30
|
+
normalized = str(string).strip().lower()
|
|
31
|
+
true_values = {"t", "true", "y", "yes", "1"}
|
|
32
|
+
false_values = {"f", "false", "n", "no", "0"}
|
|
33
|
+
if normalized in true_values:
|
|
34
|
+
return True
|
|
35
|
+
elif normalized in false_values:
|
|
36
|
+
return False
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(f"Cannot convert '{string}' to boolean")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_model(model: Any, file_name: str = "model", file_path: str = ".") -> str:
|
|
42
|
+
pickle_file = os.path.join(file_path, f"{file_name}.pkl")
|
|
43
|
+
with open(pickle_file, "wb") as file:
|
|
44
|
+
pickle.dump(model, file)
|
|
45
|
+
return pickle_file
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_model(file: str) -> Any:
|
|
49
|
+
with open(file, "rb") as model_file:
|
|
50
|
+
model = pickle.load(model_file)
|
|
51
|
+
return model
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_skills_path() -> str:
|
|
55
|
+
skills_dir = files("tunnel_manager") / "skills"
|
|
56
|
+
with as_file(skills_dir) as path:
|
|
57
|
+
skills_path = str(path)
|
|
58
|
+
return skills_path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_mcp_config_path() -> str:
|
|
62
|
+
mcp_config_file = files("tunnel_manager") / "mcp_config.json"
|
|
63
|
+
with as_file(mcp_config_file) as path:
|
|
64
|
+
mcp_config_path = str(path)
|
|
65
|
+
return mcp_config_path
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_skills_from_directory(directory: str) -> List[Skill]:
|
|
69
|
+
skills = []
|
|
70
|
+
base_path = Path(directory)
|
|
71
|
+
|
|
72
|
+
if not base_path.exists():
|
|
73
|
+
print(f"Skills directory not found: {directory}")
|
|
74
|
+
return skills
|
|
75
|
+
|
|
76
|
+
for item in base_path.iterdir():
|
|
77
|
+
if item.is_dir():
|
|
78
|
+
skill_file = item / "SKILL.md"
|
|
79
|
+
if skill_file.exists():
|
|
80
|
+
try:
|
|
81
|
+
with open(skill_file, "r") as f:
|
|
82
|
+
# Extract frontmatter
|
|
83
|
+
content = f.read()
|
|
84
|
+
if content.startswith("---"):
|
|
85
|
+
_, frontmatter, _ = content.split("---", 2)
|
|
86
|
+
data = yaml.safe_load(frontmatter)
|
|
87
|
+
|
|
88
|
+
skill_id = item.name
|
|
89
|
+
skill_name = data.get("name", skill_id)
|
|
90
|
+
skill_desc = data.get(
|
|
91
|
+
"description", f"Access to {skill_name} tools"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
tag_name = skill_id.replace("tunnel-manager-", "")
|
|
95
|
+
tags = ["tunnel-manager", tag_name]
|
|
96
|
+
|
|
97
|
+
skills.append(
|
|
98
|
+
Skill(
|
|
99
|
+
id=skill_id,
|
|
100
|
+
name=skill_name,
|
|
101
|
+
description=skill_desc,
|
|
102
|
+
tags=tags,
|
|
103
|
+
input_modes=["text"],
|
|
104
|
+
output_modes=["text"],
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"Error loading skill from {skill_file}: {e}")
|
|
109
|
+
|
|
110
|
+
return skills
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tunnel-manager
|
|
3
|
+
Version: 1.0.9
|
|
4
|
+
Summary: Create SSH Tunnels to your remote hosts and host as an MCP Server for Agentic AI!
|
|
5
|
+
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: License :: Public Domain
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: fastmcp>=2.13.0.2
|
|
16
|
+
Requires-Dist: paramiko>=4.0.0
|
|
17
|
+
Provides-Extra: mcp
|
|
18
|
+
Requires-Dist: fastmcp>=2.13.0.2; extra == "mcp"
|
|
19
|
+
Requires-Dist: eunomia-mcp>=0.3.10; extra == "mcp"
|
|
20
|
+
Provides-Extra: a2a
|
|
21
|
+
Requires-Dist: pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,huggingface,openai,web]>=1.32.0; extra == "a2a"
|
|
22
|
+
Requires-Dist: pydantic-ai-skills; extra == "a2a"
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: fastmcp>=2.13.0.2; extra == "all"
|
|
25
|
+
Requires-Dist: eunomia-mcp>=0.3.10; extra == "all"
|
|
26
|
+
Requires-Dist: pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,huggingface,openai,web]>=1.32.0; extra == "all"
|
|
27
|
+
Requires-Dist: pydantic-ai-skills; extra == "all"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Tunnel Manager - A2A | AG-UI | MCP
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+
|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
45
|
+
|
|
46
|
+

|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+

|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
*Version: 1.0.9*
|
|
54
|
+
|
|
55
|
+
## Overview
|
|
56
|
+
|
|
57
|
+
This project provides a Python-based `Tunnel` class for secure SSH connections and file transfers, integrated with a FastMCP server (`tunnel_manager_mcp.py`) to expose these capabilities as tools for AI-driven workflows. The implementation supports both standard SSH (e.g., for local networks) and Teleport's secure access platform, leveraging the `paramiko` library for SSH operations.
|
|
58
|
+
|
|
59
|
+
### Features
|
|
60
|
+
|
|
61
|
+
### Tunnel Class
|
|
62
|
+
- **Purpose**: Facilitates secure SSH connections, file transfers, and key management for single or multiple hosts.
|
|
63
|
+
- **Key Functionality**:
|
|
64
|
+
- **Run Remote Commands**: Execute shell commands on a remote host and retrieve output.
|
|
65
|
+
- **File Upload/Download**: Transfer files to/from a single host or all hosts in an inventory group using SFTP.
|
|
66
|
+
- **Passwordless SSH Setup**: Configure key-based authentication for secure, passwordless access, with support for RSA and Ed25519 key types.
|
|
67
|
+
- **SSH Config Management**: Copy local SSH config files to remote hosts.
|
|
68
|
+
- **Key Rotation**: Generate and deploy new SSH key pairs (RSA or Ed25519), updating `authorized_keys`.
|
|
69
|
+
- **Inventory Support**: Operate on multiple hosts defined in an Ansible-style YAML inventory, with group targeting (e.g., `all`, `homelab`, `poweredge`).
|
|
70
|
+
- **Teleport Support**: Seamlessly integrates with Teleport's certificate-based authentication and proxying.
|
|
71
|
+
- **Configuration Flexibility**: Loads SSH settings from `~/.ssh/config` by default, with optional overrides for username, password, identity files, certificates, and proxy commands.
|
|
72
|
+
- **Logging**: Optional file-based logging for debugging and auditing.
|
|
73
|
+
- **Parallel Execution**: Support for parallel operations across multiple hosts with configurable thread limits.
|
|
74
|
+
- **Key Type Support**: Explicit support for both RSA and Ed25519 keys in authentication, generation, and rotation for enhanced security and compatibility.
|
|
75
|
+
|
|
76
|
+
### FastMCP Server
|
|
77
|
+
- **Purpose**: Exposes `Tunnel` class functionality as a FastMCP server, enabling AI tools to perform remote operations programmatically.
|
|
78
|
+
- **Tools Provided**:
|
|
79
|
+
- `run_command_on_remote_host`: Runs a shell command on a single remote host.
|
|
80
|
+
- `send_file_to_remote_host`: Uploads a file to a single remote host via SFTP.
|
|
81
|
+
- `receive_file_from_remote_host`: Downloads a file from a single remote host via SFTP.
|
|
82
|
+
- `check_ssh_server`: Checks if the SSH server is running and configured for key-based authentication.
|
|
83
|
+
- `test_key_auth`: Tests key-based authentication for a host.
|
|
84
|
+
- `setup_passwordless_ssh`: Sets up passwordless SSH for a single host.
|
|
85
|
+
- `copy_ssh_config`: Copies an SSH config file to a single remote host.
|
|
86
|
+
- `rotate_ssh_key`: Rotates SSH keys for a single host.
|
|
87
|
+
- `remove_host_key`: Removes a host’s key from the local `known_hosts` file.
|
|
88
|
+
- `configure_key_auth_on_inventory`: Sets up passwordless SSH for all hosts in an inventory group.
|
|
89
|
+
- `run_command_on_inventory`: Runs a command on all hosts in an inventory group.
|
|
90
|
+
- `copy_ssh_config_on_inventory`: Copies an SSH config file to all hosts in an inventory group.
|
|
91
|
+
- `rotate_ssh_key_on_inventory`: Rotates SSH keys for all hosts in an inventory group.
|
|
92
|
+
- `send_file_to_inventory`: Uploads a file to all hosts in an inventory group via SFTP.
|
|
93
|
+
- `receive_file_from_inventory`: Downloads a file from all hosts in an inventory group via SFTP.
|
|
94
|
+
- **Transport Options**: Supports `stdio` (for local scripting) and `http` (for networked access) transport modes.
|
|
95
|
+
- **Progress Reporting**: Integrates with FastMCP's `Context` for progress updates during operations.
|
|
96
|
+
- **Logging**: Comprehensive logging to a file (`tunnel_mcp.log` by default) or a user-specified file.
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
### CLI
|
|
101
|
+
| Short Flag | Long Flag | Description | Required | Default Value |
|
|
102
|
+
|------------|----------------------|----------------------------------------------------------|----------|---------------|
|
|
103
|
+
| -h | --help | Show usage for the script | No | None |
|
|
104
|
+
| | --log-file | Log to specified file (default: console output) | No | Console |
|
|
105
|
+
| | setup-all | Setup passwordless SSH for all hosts in inventory | Yes* | None |
|
|
106
|
+
| | --inventory | YAML inventory path | Yes | None |
|
|
107
|
+
| | --shared-key-path | Path to shared private key | No | ~/.ssh/id_shared |
|
|
108
|
+
| | --key-type | Key type (rsa or ed25519) | No | ed25519 |
|
|
109
|
+
| | --group | Inventory group to target | No | all |
|
|
110
|
+
| | --parallel | Run operation in parallel | No | False |
|
|
111
|
+
| | --max-threads | Max threads for parallel execution | No | 5 |
|
|
112
|
+
| | run-command | Run a shell command on all hosts in inventory | Yes* | None |
|
|
113
|
+
| | --remote-command | Shell command to run | Yes | None |
|
|
114
|
+
| | copy-config | Copy SSH config to all hosts in inventory | Yes* | None |
|
|
115
|
+
| | --local-config-path | Local SSH config path | Yes | None |
|
|
116
|
+
| | --remote-config-path | Remote path for SSH config | No | ~/.ssh/config |
|
|
117
|
+
| | rotate-key | Rotate SSH keys for all hosts in inventory | Yes* | None |
|
|
118
|
+
| | --key-prefix | Prefix for new key paths (appends hostname) | No | ~/.ssh/id_ |
|
|
119
|
+
| | --key-type | Key type (rsa or ed25519) | No | ed25519 |
|
|
120
|
+
| | send-file | Upload a file to all hosts in inventory | Yes* | None |
|
|
121
|
+
| | --local-path | Local file path to upload | Yes | None |
|
|
122
|
+
| | --remote-path | Remote destination path | Yes | None |
|
|
123
|
+
| | receive-file | Download a file from all hosts in inventory | Yes* | None |
|
|
124
|
+
| | --remote-path | Remote file path to download | Yes | None |
|
|
125
|
+
| | --local-path-prefix | Local directory path prefix to save files | Yes | None |
|
|
126
|
+
|
|
127
|
+
### Notes
|
|
128
|
+
One of the commands (`setup-all`, `run-command`, `copy-config`, `rotate-key`, `send-file`, `receive-file`) must be specified as the first argument to `tunnel_manager.py`. Each command has required arguments that must be specified with flags:
|
|
129
|
+
- `setup-all`: Requires `--inventory`.
|
|
130
|
+
- `run-command`: Requires `--inventory` and `--remote-command`.
|
|
131
|
+
- `copy-config`: Requires `--inventory` and `--local-config-path`.
|
|
132
|
+
- `rotate-key`: Requires `--inventory`.
|
|
133
|
+
- `send-file`: Requires `--inventory`, `--local-path`, and `--remote-path`.
|
|
134
|
+
- `receive-file`: Requires `--inventory`, `--remote-path`, and `--local-path-prefix`.
|
|
135
|
+
|
|
136
|
+
### Additional Notes
|
|
137
|
+
- Ensure `ansible_host` values in `inventory.yml` are resolvable IPs or hostnames.
|
|
138
|
+
- Update `ansible_ssh_private_key_file` in the inventory after running `rotate-key`.
|
|
139
|
+
- Use `--log-file` for file-based logging or omit for console output.
|
|
140
|
+
- The `--parallel` option speeds up operations but may overload resources; adjust `--max-threads` as needed.
|
|
141
|
+
- The `receive-file` command saves files to `local_path_prefix/<hostname>/<filename>` to preserve original filenames and avoid conflicts.
|
|
142
|
+
- Ed25519 keys are recommended for better security and performance over RSA, but RSA is supported for compatibility with older systems.
|
|
143
|
+
|
|
144
|
+
#### 1. Setup Passwordless SSH
|
|
145
|
+
Set up passwordless SSH for hosts in the inventory, distributing a shared key. Use `--key-type` to specify RSA or Ed25519 (default: ed25519).
|
|
146
|
+
- **Target `all` group (sequential, Ed25519)**:
|
|
147
|
+
```bash
|
|
148
|
+
tunnel-manager setup-all --inventory inventory.yml --shared-key-path ~/.ssh/id_shared --key-type ed25519
|
|
149
|
+
```
|
|
150
|
+
- **Target `homelab` group (parallel, 3 threads, RSA)**:
|
|
151
|
+
```bash
|
|
152
|
+
tunnel-manager setup-all --inventory inventory.yml --shared-key-path ~/.ssh/id_shared_rsa --key-type rsa --group homelab --parallel --max-threads 3
|
|
153
|
+
```
|
|
154
|
+
- **Target `poweredge` group (sequential, Ed25519)**:
|
|
155
|
+
```bash
|
|
156
|
+
tunnel-manager --log-file setup_poweredge.log setup-all --inventory inventory.yml --shared-key-path ~/.ssh/id_shared --key-type ed25519 --group poweredge
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### 2. Run a Command
|
|
160
|
+
Execute a shell command on all hosts in the specified group.
|
|
161
|
+
- **Run `uptime` on `all` group (sequential)**:
|
|
162
|
+
```bash
|
|
163
|
+
tunnel-manager run-command --inventory inventory.yml --remote-command "uptime"
|
|
164
|
+
```
|
|
165
|
+
- **Run `df -h` on `homelab` group (parallel, 5 threads)**:
|
|
166
|
+
```bash
|
|
167
|
+
tunnel-manager run-command --inventory inventory.yml --remote-command "df -h" --group homelab --parallel --max-threads 5
|
|
168
|
+
```
|
|
169
|
+
- **Run `whoami` on `poweredge` group (sequential)**:
|
|
170
|
+
```bash
|
|
171
|
+
tunnel-manager run-command --inventory inventory.yml --remote-command "whoami" --group poweredge
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### 3. Copy SSH Config
|
|
175
|
+
Copy a local SSH config file to the remote hosts’ `~/.ssh/config`.
|
|
176
|
+
- **Copy to `all` group (sequential)**:
|
|
177
|
+
```bash
|
|
178
|
+
tunnel-manager copy-config --inventory inventory.yml --local-config-path ~/.ssh/config
|
|
179
|
+
```
|
|
180
|
+
- **Copy to `homelab` group (parallel, 4 threads)**:
|
|
181
|
+
```bash
|
|
182
|
+
tunnel-manager copy-config --inventory inventory.yml --local-config-path ~/.ssh/config --group homelab --parallel --max-threads 4
|
|
183
|
+
```
|
|
184
|
+
- **Copy to `poweredge` group with custom remote path**:
|
|
185
|
+
```bash
|
|
186
|
+
tunnel-manager --log-file copy_config.log copy-config --inventory inventory.yml --local-config-path ~/.ssh/config --remote-config-path ~/.ssh/custom_config --group poweredge
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### 4. Rotate SSH Keys
|
|
190
|
+
Rotate SSH keys for hosts, generating new keys with a prefix. Use `--key-type` to specify RSA or Ed25519 (default: ed25519).
|
|
191
|
+
- **Rotate keys for `all` group (sequential, Ed25519)**:
|
|
192
|
+
```bash
|
|
193
|
+
tunnel-manager rotate-key --inventory inventory.yml --key-prefix ~/.ssh/id_ --key-type ed25519
|
|
194
|
+
```
|
|
195
|
+
- **Rotate keys for `homelab` group (parallel, 3 threads, RSA)**:
|
|
196
|
+
```bash
|
|
197
|
+
tunnel-manager rotate-key --inventory inventory.yml --key-prefix ~/.ssh/id_rsa_ --key-type rsa --group homelab --parallel --max-threads 3
|
|
198
|
+
```
|
|
199
|
+
- **Rotate keys for `poweredge` group (sequential, Ed25519)**:
|
|
200
|
+
```bash
|
|
201
|
+
tunnel-manager --log-file rotate.log rotate-key --inventory inventory.yml --key-prefix ~/.ssh/id_ --key-type ed25519 --group poweredge
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### 5. Upload a File
|
|
205
|
+
Upload a local file to all hosts in the specified group.
|
|
206
|
+
- **Upload to `all` group (sequential)**:
|
|
207
|
+
```bash
|
|
208
|
+
tunnel-manager send-file --inventory inventory.yml --local-path ./myfile.txt --remote-path /home/user/myfile.txt
|
|
209
|
+
```
|
|
210
|
+
- **Upload to `homelab` group (parallel, 3 threads)**:
|
|
211
|
+
```bash
|
|
212
|
+
tunnel-manager send-file --inventory inventory.yml --local-path ./myfile.txt --remote-path /home/user/myfile.txt --group homelab --parallel --max-threads 3
|
|
213
|
+
```
|
|
214
|
+
- **Upload to `poweredge` group (sequential)**:
|
|
215
|
+
```bash
|
|
216
|
+
tunnel-manager --log-file upload_poweredge.log send-file --inventory inventory.yml --local-path ./myfile.txt --remote-path /home/user/myfile.txt --group poweredge
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### 6. Download a File
|
|
220
|
+
Download a file from all hosts in the specified group, saving to host-specific subdirectories (e.g., `downloads/R510/myfile.txt`).
|
|
221
|
+
- **Download from `all` group (sequential)**:
|
|
222
|
+
```bash
|
|
223
|
+
tunnel-manager receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads
|
|
224
|
+
```
|
|
225
|
+
- **Download from `homelab` group (parallel, 3 threads)**:
|
|
226
|
+
```bash
|
|
227
|
+
tunnel-manager receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads --group homelab --parallel --max-threads 3
|
|
228
|
+
```
|
|
229
|
+
- **Download from `poweredge` group (sequential)**:
|
|
230
|
+
```bash
|
|
231
|
+
tunnel-manager --log-file download_poweredge.log receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads --group poweredge
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Tunnel Manager Inventory
|
|
235
|
+
|
|
236
|
+
**Inventory File Example (`inventory.yml`)**:
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
all:
|
|
240
|
+
hosts:
|
|
241
|
+
r510:
|
|
242
|
+
ansible_host: 192.168.1.10
|
|
243
|
+
ansible_user: admin
|
|
244
|
+
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
245
|
+
r710:
|
|
246
|
+
ansible_host: 192.168.1.11
|
|
247
|
+
ansible_user: admin
|
|
248
|
+
ansible_ssh_pass: mypassword
|
|
249
|
+
gr1080:
|
|
250
|
+
ansible_host: 192.168.1.14
|
|
251
|
+
ansible_user: admin
|
|
252
|
+
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
|
|
253
|
+
homelab:
|
|
254
|
+
hosts:
|
|
255
|
+
r510:
|
|
256
|
+
ansible_host: 192.168.1.10
|
|
257
|
+
ansible_user: admin
|
|
258
|
+
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
259
|
+
r710:
|
|
260
|
+
ansible_host: 192.168.1.11
|
|
261
|
+
ansible_user: admin
|
|
262
|
+
ansible_ssh_pass: mypassword
|
|
263
|
+
gr1080:
|
|
264
|
+
ansible_host: 192.168.1.14
|
|
265
|
+
ansible_user: admin
|
|
266
|
+
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
|
|
267
|
+
poweredge:
|
|
268
|
+
hosts:
|
|
269
|
+
r510:
|
|
270
|
+
ansible_host: 192.168.1.10
|
|
271
|
+
ansible_user: admin
|
|
272
|
+
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
273
|
+
r710:
|
|
274
|
+
ansible_host: 192.168.1.11
|
|
275
|
+
ansible_user: admin
|
|
276
|
+
ansible_ssh_pass: mypassword
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Replace IPs, usernames, and passwords with your actual values.
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
### MCP CLI
|
|
283
|
+
|
|
284
|
+
| Short Flag | Long Flag | Description |
|
|
285
|
+
|------------|------------------------------------|-----------------------------------------------------------------------------|
|
|
286
|
+
| -h | --help | Display help information |
|
|
287
|
+
| -t | --transport | Transport method: 'stdio', 'http', or 'sse' [legacy] (default: stdio) |
|
|
288
|
+
| -s | --host | Host address for HTTP transport (default: 0.0.0.0) |
|
|
289
|
+
| -p | --port | Port number for HTTP transport (default: 8000) |
|
|
290
|
+
| | --auth-type | Authentication type: 'none', 'static', 'jwt', 'oauth-proxy', 'oidc-proxy', 'remote-oauth' (default: none) |
|
|
291
|
+
| | --token-jwks-uri | JWKS URI for JWT verification |
|
|
292
|
+
| | --token-issuer | Issuer for JWT verification |
|
|
293
|
+
| | --token-audience | Audience for JWT verification |
|
|
294
|
+
| | --oauth-upstream-auth-endpoint | Upstream authorization endpoint for OAuth Proxy |
|
|
295
|
+
| | --oauth-upstream-token-endpoint | Upstream token endpoint for OAuth Proxy |
|
|
296
|
+
| | --oauth-upstream-client-id | Upstream client ID for OAuth Proxy |
|
|
297
|
+
| | --oauth-upstream-client-secret | Upstream client secret for OAuth Proxy |
|
|
298
|
+
| | --oauth-base-url | Base URL for OAuth Proxy |
|
|
299
|
+
| | --oidc-config-url | OIDC configuration URL |
|
|
300
|
+
| | --oidc-client-id | OIDC client ID |
|
|
301
|
+
| | --oidc-client-secret | OIDC client secret |
|
|
302
|
+
| | --oidc-base-url | Base URL for OIDC Proxy |
|
|
303
|
+
| | --remote-auth-servers | Comma-separated list of authorization servers for Remote OAuth |
|
|
304
|
+
| | --remote-base-url | Base URL for Remote OAuth |
|
|
305
|
+
| | --allowed-client-redirect-uris | Comma-separated list of allowed client redirect URIs |
|
|
306
|
+
| | --eunomia-type | Eunomia authorization type: 'none', 'embedded', 'remote' (default: none) |
|
|
307
|
+
| | --eunomia-policy-file | Policy file for embedded Eunomia (default: mcp_policies.json) |
|
|
308
|
+
| | --eunomia-remote-url | URL for remote Eunomia server |
|
|
309
|
+
|
|
310
|
+
### Using as an MCP Server
|
|
311
|
+
|
|
312
|
+
The MCP Server can be run in two modes: `stdio` (for local testing) or `http` (for networked access). To start the server, use the following commands:
|
|
313
|
+
|
|
314
|
+
### A2A CLI
|
|
315
|
+
#### Endpoints
|
|
316
|
+
- **Web UI**: `http://localhost:8000/` (if enabled)
|
|
317
|
+
- **A2A**: `http://localhost:8000/a2a` (Discovery: `/a2a/.well-known/agent.json`)
|
|
318
|
+
- **AG-UI**: `http://localhost:8000/ag-ui` (POST)
|
|
319
|
+
|
|
320
|
+
| Short Flag | Long Flag | Description |
|
|
321
|
+
|------------|-------------------|------------------------------------------------------------------------|
|
|
322
|
+
| -h | --help | Display help information |
|
|
323
|
+
| | --host | Host to bind the server to (default: 0.0.0.0) |
|
|
324
|
+
| | --port | Port to bind the server to (default: 9000) |
|
|
325
|
+
| | --reload | Enable auto-reload |
|
|
326
|
+
| | --provider | LLM Provider: 'openai', 'anthropic', 'google', 'huggingface' |
|
|
327
|
+
| | --model-id | LLM Model ID (default: qwen3:4b) |
|
|
328
|
+
| | --base-url | LLM Base URL (for OpenAI compatible providers) |
|
|
329
|
+
| | --api-key | LLM API Key |
|
|
330
|
+
| | --mcp-url | MCP Server URL (default: http://localhost:8000/mcp) |
|
|
331
|
+
| | --web | Enable Pydantic AI Web UI | False (Env: ENABLE_WEB_UI) |
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
#### Run in stdio mode (default):
|
|
337
|
+
```bash
|
|
338
|
+
tunnel-manager-mcp --transport "stdio"
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
#### Run in HTTP mode:
|
|
342
|
+
```bash
|
|
343
|
+
tunnel-manager-mcp --transport "http" --host "0.0.0.0" --port "8000"
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Tunnel Class
|
|
347
|
+
The `Tunnel` class can be used standalone for SSH operations. Examples:
|
|
348
|
+
|
|
349
|
+
#### Using RSA Keys
|
|
350
|
+
```python
|
|
351
|
+
from tunnel_manager.tunnel_manager import Tunnel
|
|
352
|
+
|
|
353
|
+
# Initialize with a remote host (assumes ~/.ssh/config or explicit params)
|
|
354
|
+
tunnel = Tunnel(
|
|
355
|
+
remote_host="192.168.1.10",
|
|
356
|
+
username="admin",
|
|
357
|
+
password="mypassword",
|
|
358
|
+
identity_file="/path/to/id_rsa",
|
|
359
|
+
certificate_file="/path/to/cert", # Optional for Teleport
|
|
360
|
+
proxy_command="tsh proxy ssh %h", # Optional for Teleport
|
|
361
|
+
ssh_config_file="~/.ssh/config",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Connect and run a command
|
|
365
|
+
tunnel.connect()
|
|
366
|
+
out, err = tunnel.run_command("ls -la /tmp")
|
|
367
|
+
print(f"Output: {out}\nError: {err}")
|
|
368
|
+
|
|
369
|
+
# Upload a file
|
|
370
|
+
tunnel.send_file("/local/file.txt", "/remote/file.txt")
|
|
371
|
+
|
|
372
|
+
# Download a file
|
|
373
|
+
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
|
|
374
|
+
|
|
375
|
+
# Setup passwordless SSH with RSA
|
|
376
|
+
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_rsa", key_type="rsa")
|
|
377
|
+
|
|
378
|
+
# Copy SSH config
|
|
379
|
+
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
|
|
380
|
+
|
|
381
|
+
# Rotate SSH key with RSA
|
|
382
|
+
tunnel.rotate_ssh_key("/path/to/new_rsa_key", key_type="rsa")
|
|
383
|
+
|
|
384
|
+
# Close the connection
|
|
385
|
+
tunnel.close()
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Using Ed25519 Keys
|
|
389
|
+
```python
|
|
390
|
+
from tunnel_manager.tunnel_manager import Tunnel
|
|
391
|
+
|
|
392
|
+
# Initialize with a remote host (assumes ~/.ssh/config or explicit params)
|
|
393
|
+
tunnel = Tunnel(
|
|
394
|
+
remote_host="192.168.1.10",
|
|
395
|
+
username="admin",
|
|
396
|
+
password="mypassword",
|
|
397
|
+
identity_file="/path/to/id_ed25519",
|
|
398
|
+
certificate_file="/path/to/cert", # Optional for Teleport
|
|
399
|
+
proxy_command="tsh proxy ssh %h", # Optional for Teleport
|
|
400
|
+
ssh_config_file="~/.ssh/config",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Connect and run a command
|
|
404
|
+
tunnel.connect()
|
|
405
|
+
out, err = tunnel.run_command("ls -la /tmp")
|
|
406
|
+
print(f"Output: {out}\nError: {err}")
|
|
407
|
+
|
|
408
|
+
# Upload a file
|
|
409
|
+
tunnel.send_file("/local/file.txt", "/remote/file.txt")
|
|
410
|
+
|
|
411
|
+
# Download a file
|
|
412
|
+
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
|
|
413
|
+
|
|
414
|
+
# Setup passwordless SSH with Ed25519
|
|
415
|
+
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_ed25519", key_type="ed25519")
|
|
416
|
+
|
|
417
|
+
# Copy SSH config
|
|
418
|
+
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
|
|
419
|
+
|
|
420
|
+
# Rotate SSH key with Ed25519
|
|
421
|
+
tunnel.rotate_ssh_key("/path/to/new_ed25519_key", key_type="ed25519")
|
|
422
|
+
|
|
423
|
+
# Close the connection
|
|
424
|
+
tunnel.close()
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Deploy MCP Server as a Service
|
|
428
|
+
|
|
429
|
+
The MCP server can be deployed using Docker, with configurable authentication, middleware, and Eunomia authorization.
|
|
430
|
+
|
|
431
|
+
#### Using Docker Run
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
docker pull knucklessg1/tunnel-manager:latest
|
|
435
|
+
|
|
436
|
+
docker run -d \
|
|
437
|
+
--name tunnel-manager-mcp \
|
|
438
|
+
-p 8004:8004 \
|
|
439
|
+
-e HOST=0.0.0.0 \
|
|
440
|
+
-e PORT=8004 \
|
|
441
|
+
-e TRANSPORT=http \
|
|
442
|
+
-e AUTH_TYPE=none \
|
|
443
|
+
-e EUNOMIA_TYPE=none \
|
|
444
|
+
knucklessg1/tunnel-manager:latest
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
For advanced authentication (e.g., JWT, OAuth Proxy, OIDC Proxy, Remote OAuth) or Eunomia, add the relevant environment variables:
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
docker run -d \
|
|
451
|
+
--name tunnel-manager-mcp \
|
|
452
|
+
-p 8004:8004 \
|
|
453
|
+
-e HOST=0.0.0.0 \
|
|
454
|
+
-e PORT=8004 \
|
|
455
|
+
-e TRANSPORT=http \
|
|
456
|
+
-e AUTH_TYPE=oidc-proxy \
|
|
457
|
+
-e OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration \
|
|
458
|
+
-e OIDC_CLIENT_ID=your-client-id \
|
|
459
|
+
-e OIDC_CLIENT_SECRET=your-client-secret \
|
|
460
|
+
-e OIDC_BASE_URL=https://your-server.com \
|
|
461
|
+
-e ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/* \
|
|
462
|
+
-e EUNOMIA_TYPE=embedded \
|
|
463
|
+
-e EUNOMIA_POLICY_FILE=/app/mcp_policies.json \
|
|
464
|
+
knucklessg1/tunnel-manager:latest
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
#### Using Docker Compose
|
|
468
|
+
|
|
469
|
+
Create a `docker-compose.yml` file:
|
|
470
|
+
|
|
471
|
+
```yaml
|
|
472
|
+
services:
|
|
473
|
+
tunnel-manager-mcp:
|
|
474
|
+
image: knucklessg1/tunnel-manager:latest
|
|
475
|
+
environment:
|
|
476
|
+
- HOST=0.0.0.0
|
|
477
|
+
- PORT=8004
|
|
478
|
+
- TRANSPORT=http
|
|
479
|
+
- AUTH_TYPE=none
|
|
480
|
+
- EUNOMIA_TYPE=none
|
|
481
|
+
ports:
|
|
482
|
+
- 8004:8004
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
For advanced setups with authentication and Eunomia:
|
|
486
|
+
|
|
487
|
+
```yaml
|
|
488
|
+
services:
|
|
489
|
+
tunnel-manager-mcp:
|
|
490
|
+
image: knucklessg1/tunnel-manager:latest
|
|
491
|
+
environment:
|
|
492
|
+
- HOST=0.0.0.0
|
|
493
|
+
- PORT=8004
|
|
494
|
+
- TRANSPORT=http
|
|
495
|
+
- AUTH_TYPE=oidc-proxy
|
|
496
|
+
- OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration
|
|
497
|
+
- OIDC_CLIENT_ID=your-client-id
|
|
498
|
+
- OIDC_CLIENT_SECRET=your-client-secret
|
|
499
|
+
- OIDC_BASE_URL=https://your-server.com
|
|
500
|
+
- ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/*
|
|
501
|
+
- EUNOMIA_TYPE=embedded
|
|
502
|
+
- EUNOMIA_POLICY_FILE=/app/mcp_policies.json
|
|
503
|
+
ports:
|
|
504
|
+
- 8004:8004
|
|
505
|
+
volumes:
|
|
506
|
+
- ./mcp_policies.json:/app/mcp_policies.json
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Run the service:
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
docker-compose up -d
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
#### Configure `mcp.json` for AI Integration
|
|
516
|
+
|
|
517
|
+
```json
|
|
518
|
+
{
|
|
519
|
+
"mcpServers": {
|
|
520
|
+
"tunnel_manager": {
|
|
521
|
+
"command": "uv",
|
|
522
|
+
"args": [
|
|
523
|
+
"run",
|
|
524
|
+
"--with",
|
|
525
|
+
"tunnel-manager",
|
|
526
|
+
"tunnel_manager_mcp"
|
|
527
|
+
],
|
|
528
|
+
"env": {
|
|
529
|
+
"TUNNEL_REMOTE_HOST": "192.168.1.12", // Optional
|
|
530
|
+
"TUNNEL_USERNAME": "admin", // Optional
|
|
531
|
+
"TUNNEL_PASSWORD": "", // Optional
|
|
532
|
+
"TUNNEL_REMOTE_PORT": "22", // Optional
|
|
533
|
+
"TUNNEL_IDENTITY_FILE": "", // Optional
|
|
534
|
+
"TUNNEL_INVENTORY": "~/inventory.yaml", // Optional
|
|
535
|
+
"TUNNEL_INVENTORY_GROUP": "all", // Optional
|
|
536
|
+
"TUNNEL_PARALLEL": "true", // Optional
|
|
537
|
+
"TUNNEL_CERTIFICATE": "", // Optional
|
|
538
|
+
"TUNNEL_PROXY_COMMAND": "", // Optional
|
|
539
|
+
"TUNNEL_LOG_FILE": "~/tunnel_log.txt", // Optional
|
|
540
|
+
"TUNNEL_MAX_THREADS": "6" // Optional
|
|
541
|
+
},
|
|
542
|
+
"timeout": 200000
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## Install Python Package
|
|
549
|
+
```bash
|
|
550
|
+
python -m pip install tunnel-manager
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
or
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
uv pip install --upgrade tunnel-manager
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Repository Owners
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
<img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
|
|
563
|
+
|
|
564
|
+

|
|
565
|
+

|