universal-host-manager-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ # Bind to loopback when a reverse proxy or tunnel runs on the same host.
2
+ HOST=127.0.0.1
3
+ PORT=8765
4
+ MCP_BASE_URL=https://mcp.example.com
5
+ MCP_WORKSPACE_DIR=/home/youruser/workspace
6
+
7
+ MAX_OUTPUT_CHARS=64000
8
+ DEFAULT_CMD_TIMEOUT=300
9
+ MAX_CMD_TIMEOUT=1800
10
+ MAX_READ_BYTES=5000000
11
+ MAX_WRITE_BYTES=5000000
12
+ LOG_LEVEL=INFO
13
+
14
+ AUTH0_DOMAIN=your-tenant.eu.auth0.com
15
+ AUTH0_CLIENT_ID=replace_me
16
+ AUTH0_CLIENT_SECRET=replace_me
17
+ AUTH0_AUDIENCE=https://mcp.example.com/
18
+
19
+ # Local development only. Never enable on a publicly reachable endpoint.
20
+ ALLOW_INSECURE_NO_AUTH=false
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ .venv/
6
+ venv/
7
+ __pycache__/
8
+ *.py[cod]
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ htmlcov/
14
+ dist/
15
+ build/
16
+ *.egg-info/
17
+ .idea/
18
+ .vscode/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmet Baktiaya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.5
2
+ Name: universal-host-manager-mcp
3
+ Version: 0.1.0
4
+ Summary: Cross-platform FastMCP server for authenticated remote Linux/macOS host administration
5
+ Project-URL: Homepage, https://github.com/Abktya/universal-host-manager-mcp
6
+ Project-URL: Repository, https://github.com/Abktya/universal-host-manager-mcp
7
+ Project-URL: Issues, https://github.com/Abktya/universal-host-manager-mcp/issues
8
+ Project-URL: Security, https://github.com/Abktya/universal-host-manager-mcp/blob/main/SECURITY.md
9
+ Author: Ahmet Baktiaya
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Ahmet Baktiaya
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: chatgpt,claude,devops,mcp,model-context-protocol,remote-shell,sysadmin
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: System Administrators
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: MacOS
37
+ Classifier: Operating System :: POSIX :: Linux
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: System :: Systems Administration
42
+ Requires-Python: >=3.10
43
+ Requires-Dist: fastmcp<5,>=2.13
44
+ Requires-Dist: python-dotenv<2,>=1.0
45
+ Description-Content-Type: text/markdown
46
+
47
+ # Universal Host Manager MCP
48
+
49
+ A cross-platform [Model Context Protocol](https://modelcontextprotocol.io/) server for administering a Linux or macOS host through MCP clients such as ChatGPT and Claude.
50
+
51
+ It uses [FastMCP](https://gofastmcp.com/) Streamable HTTP transport, Auth0 OAuth, bounded file tools, output limits and command timeouts.
52
+
53
+ > [!CAUTION]
54
+ > This project exposes arbitrary shell execution. Authentication decides who may use it; it does not make commands harmless. Read [SECURITY.md](SECURITY.md) before deploying it.
55
+
56
+ ## Features
57
+
58
+ - Linux and macOS support
59
+ - Streamable HTTP endpoint
60
+ - Auth0 OAuth integration
61
+ - File reads, writes and directory listings constrained to `MCP_WORKSPACE_DIR`
62
+ - Configurable command timeouts and output truncation
63
+ - File-size limits and decoding fallback
64
+ - Fail-closed startup when authentication is not configured
65
+ - Example systemd and launchd services
66
+
67
+ ## Tools
68
+
69
+ | Tool | Parameters | Purpose |
70
+ | --- | --- | --- |
71
+ | `run_command` | `command: str`, `timeout: int` | Runs an arbitrary shell command with the workspace as its working directory |
72
+ | `read_file` | `path: str` | Reads a text file inside the workspace |
73
+ | `write_file` | `path: str`, `content: str` | Writes UTF-8 text inside the workspace |
74
+ | `list_dir` | `path: str = "."` | Lists a directory inside the workspace |
75
+ | `system_metrics` | none | Reports disk, memory and top-process information |
76
+
77
+ The workspace boundary applies to the file tools. It does **not** sandbox `run_command`; commands retain all permissions of the service's OS user.
78
+
79
+ ## Requirements
80
+
81
+ - Python 3.10+
82
+ - Linux or macOS
83
+ - Auth0 account for remote use
84
+ - HTTPS endpoint for remote MCP clients
85
+
86
+ ## Install
87
+
88
+ ```bash
89
+ git clone https://github.com/Abktya/universal-host-manager-mcp.git
90
+ cd universal-host-manager-mcp
91
+ python3 -m venv .venv
92
+ source .venv/bin/activate
93
+ python -m pip install --upgrade pip
94
+ pip install -r requirements.txt
95
+ cp .env.example .env
96
+ ```
97
+
98
+ Edit `.env` and use an explicitly restricted workspace:
99
+
100
+ ```dotenv
101
+ HOST=127.0.0.1
102
+ PORT=8765
103
+ MCP_BASE_URL=https://mcp.example.com
104
+ MCP_WORKSPACE_DIR=/home/youruser/workspace
105
+
106
+ AUTH0_DOMAIN=your-tenant.eu.auth0.com
107
+ AUTH0_CLIENT_ID=replace_me
108
+ AUTH0_CLIENT_SECRET=replace_me
109
+ AUTH0_AUDIENCE=https://mcp.example.com/
110
+ ```
111
+
112
+ Never commit `.env`.
113
+
114
+ ## Auth0 setup
115
+
116
+ This project uses FastMCP's `Auth0Provider` fixed-client OAuth integration.
117
+
118
+ 1. In Auth0, create an API.
119
+ 2. Use your public MCP URL as its identifier/audience, for example `https://mcp.example.com/`.
120
+ 3. Create a Regular Web Application.
121
+ 4. Put its domain, client ID and client secret in `.env`.
122
+ 5. Add only the callback URLs required by your MCP clients to the Auth0 application's allowed callback URLs.
123
+ 6. Set the application's allowed web origins and logout URLs as required by your clients.
124
+ 7. Keep RS256 signing enabled.
125
+
126
+ FastMCP also supports an Auth0 MCP-native/DCR path through `Auth0MCPProvider`. This repository currently uses the manually managed, fixed-client `Auth0Provider` path.
127
+
128
+ ## Install
129
+
130
+ From PyPI (recommended):
131
+
132
+ ```bash
133
+ pip install universal-host-manager-mcp
134
+ ```
135
+
136
+ Or with [uv](https://docs.astral.sh/uv/) / [pipx](https://pipx.pypa.io/), without polluting a project environment:
137
+
138
+ ```bash
139
+ uvx universal-host-manager-mcp
140
+ ```
141
+
142
+ From source (for development):
143
+
144
+ ```bash
145
+ python -m venv .venv
146
+ source .venv/bin/activate
147
+ pip install -e .
148
+ ```
149
+
150
+ ## Run
151
+
152
+ ```bash
153
+ universal-host-manager-mcp
154
+ ```
155
+
156
+ (Running from a source checkout with the `.venv` activated works the same way — the console script is installed by `pip install -e .`.)
157
+
158
+ With the default port, the Streamable HTTP endpoint is:
159
+
160
+ ```text
161
+ http://127.0.0.1:8765/mcp
162
+ ```
163
+
164
+ For an intentional local-only test without Auth0:
165
+
166
+ ```bash
167
+ ALLOW_INSECURE_NO_AUTH=true universal-host-manager-mcp
168
+ ```
169
+
170
+ Do not use insecure mode on a publicly reachable endpoint.
171
+
172
+ ## Cloudflare Tunnel
173
+
174
+ Install `cloudflared`, authenticate it and create a named tunnel:
175
+
176
+ ```bash
177
+ cloudflared tunnel login
178
+ cloudflared tunnel create universal-host-manager-mcp
179
+ cloudflared tunnel route dns universal-host-manager-mcp mcp.example.com
180
+ ```
181
+
182
+ Create `~/.cloudflared/config.yml`:
183
+
184
+ ```yaml
185
+ tunnel: YOUR_TUNNEL_ID
186
+ credentials-file: /home/youruser/.cloudflared/YOUR_TUNNEL_ID.json
187
+
188
+ ingress:
189
+ - hostname: mcp.example.com
190
+ service: http://127.0.0.1:8765
191
+ - service: http_status:404
192
+ ```
193
+
194
+ Validate and run it:
195
+
196
+ ```bash
197
+ cloudflared tunnel ingress validate
198
+ cloudflared tunnel run universal-host-manager-mcp
199
+ ```
200
+
201
+ Your remote MCP URL will be:
202
+
203
+ ```text
204
+ https://mcp.example.com/mcp
205
+ ```
206
+
207
+ Set `MCP_BASE_URL=https://mcp.example.com`; do not include `/mcp` in `MCP_BASE_URL`.
208
+
209
+ ## ChatGPT and Claude
210
+
211
+ Add the public Streamable HTTP URL to the client's MCP/connector configuration:
212
+
213
+ ```text
214
+ https://mcp.example.com/mcp
215
+ ```
216
+
217
+ Complete the Auth0 sign-in when the client opens the authorization flow. The exact settings screens and supported connector options can change, so follow the current client documentation rather than using legacy SSE instructions.
218
+
219
+ Multiple clients can connect to the same running HTTP server. Each client authenticates independently; no separate server process or port is required.
220
+
221
+ ## Background service
222
+
223
+ ### Linux systemd
224
+
225
+ Copy and edit the included unit:
226
+
227
+ ```bash
228
+ sudo cp examples/mcp-manager.service /etc/systemd/system/
229
+ sudo systemctl daemon-reload
230
+ sudo systemctl enable --now mcp-manager
231
+ sudo systemctl status mcp-manager --no-pager
232
+ ```
233
+
234
+ The example uses systemd hardening directives. Adjust `ReadWritePaths`, `ProtectHome`, the user, paths and permissions to match the resources the MCP server genuinely needs.
235
+
236
+ ### macOS launchd
237
+
238
+ Edit paths in `examples/com.user.mcpmanager.plist`, then:
239
+
240
+ ```bash
241
+ cp examples/com.user.mcpmanager.plist ~/Library/LaunchAgents/
242
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.mcpmanager.plist
243
+ launchctl kickstart -k gui/$(id -u)/com.user.mcpmanager
244
+ ```
245
+
246
+ ## Configuration
247
+
248
+ | Variable | Default | Description |
249
+ | --- | --- | --- |
250
+ | `HOST` | `127.0.0.1` | Listen address |
251
+ | `PORT` | `8765` | Listen port |
252
+ | `MCP_BASE_URL` | local URL | Public OAuth base URL, without `/mcp` |
253
+ | `MCP_WORKSPACE_DIR` | user home | Boundary for file tools and command working directory |
254
+ | `MAX_OUTPUT_CHARS` | `64000` | Maximum returned tool-output characters |
255
+ | `DEFAULT_CMD_TIMEOUT` | `300` | Default command timeout in seconds |
256
+ | `MAX_CMD_TIMEOUT` | `1800` | Maximum accepted command timeout |
257
+ | `MAX_READ_BYTES` | `5000000` | Maximum file size read by `read_file` |
258
+ | `MAX_WRITE_BYTES` | `5000000` | Maximum content size written by `write_file` |
259
+ | `LOG_LEVEL` | `INFO` | Python log level |
260
+ | `ALLOW_INSECURE_NO_AUTH` | `false` | Explicit local-development authentication bypass |
261
+
262
+ ## Download
263
+
264
+ Clone with Git:
265
+
266
+ ```bash
267
+ git clone https://github.com/Abktya/universal-host-manager-mcp.git
268
+ ```
269
+
270
+ Or use **Code → Download ZIP** on GitHub.
271
+
272
+ ## License
273
+
274
+ [MIT](LICENSE)
@@ -0,0 +1,228 @@
1
+ # Universal Host Manager MCP
2
+
3
+ A cross-platform [Model Context Protocol](https://modelcontextprotocol.io/) server for administering a Linux or macOS host through MCP clients such as ChatGPT and Claude.
4
+
5
+ It uses [FastMCP](https://gofastmcp.com/) Streamable HTTP transport, Auth0 OAuth, bounded file tools, output limits and command timeouts.
6
+
7
+ > [!CAUTION]
8
+ > This project exposes arbitrary shell execution. Authentication decides who may use it; it does not make commands harmless. Read [SECURITY.md](SECURITY.md) before deploying it.
9
+
10
+ ## Features
11
+
12
+ - Linux and macOS support
13
+ - Streamable HTTP endpoint
14
+ - Auth0 OAuth integration
15
+ - File reads, writes and directory listings constrained to `MCP_WORKSPACE_DIR`
16
+ - Configurable command timeouts and output truncation
17
+ - File-size limits and decoding fallback
18
+ - Fail-closed startup when authentication is not configured
19
+ - Example systemd and launchd services
20
+
21
+ ## Tools
22
+
23
+ | Tool | Parameters | Purpose |
24
+ | --- | --- | --- |
25
+ | `run_command` | `command: str`, `timeout: int` | Runs an arbitrary shell command with the workspace as its working directory |
26
+ | `read_file` | `path: str` | Reads a text file inside the workspace |
27
+ | `write_file` | `path: str`, `content: str` | Writes UTF-8 text inside the workspace |
28
+ | `list_dir` | `path: str = "."` | Lists a directory inside the workspace |
29
+ | `system_metrics` | none | Reports disk, memory and top-process information |
30
+
31
+ The workspace boundary applies to the file tools. It does **not** sandbox `run_command`; commands retain all permissions of the service's OS user.
32
+
33
+ ## Requirements
34
+
35
+ - Python 3.10+
36
+ - Linux or macOS
37
+ - Auth0 account for remote use
38
+ - HTTPS endpoint for remote MCP clients
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ git clone https://github.com/Abktya/universal-host-manager-mcp.git
44
+ cd universal-host-manager-mcp
45
+ python3 -m venv .venv
46
+ source .venv/bin/activate
47
+ python -m pip install --upgrade pip
48
+ pip install -r requirements.txt
49
+ cp .env.example .env
50
+ ```
51
+
52
+ Edit `.env` and use an explicitly restricted workspace:
53
+
54
+ ```dotenv
55
+ HOST=127.0.0.1
56
+ PORT=8765
57
+ MCP_BASE_URL=https://mcp.example.com
58
+ MCP_WORKSPACE_DIR=/home/youruser/workspace
59
+
60
+ AUTH0_DOMAIN=your-tenant.eu.auth0.com
61
+ AUTH0_CLIENT_ID=replace_me
62
+ AUTH0_CLIENT_SECRET=replace_me
63
+ AUTH0_AUDIENCE=https://mcp.example.com/
64
+ ```
65
+
66
+ Never commit `.env`.
67
+
68
+ ## Auth0 setup
69
+
70
+ This project uses FastMCP's `Auth0Provider` fixed-client OAuth integration.
71
+
72
+ 1. In Auth0, create an API.
73
+ 2. Use your public MCP URL as its identifier/audience, for example `https://mcp.example.com/`.
74
+ 3. Create a Regular Web Application.
75
+ 4. Put its domain, client ID and client secret in `.env`.
76
+ 5. Add only the callback URLs required by your MCP clients to the Auth0 application's allowed callback URLs.
77
+ 6. Set the application's allowed web origins and logout URLs as required by your clients.
78
+ 7. Keep RS256 signing enabled.
79
+
80
+ FastMCP also supports an Auth0 MCP-native/DCR path through `Auth0MCPProvider`. This repository currently uses the manually managed, fixed-client `Auth0Provider` path.
81
+
82
+ ## Install
83
+
84
+ From PyPI (recommended):
85
+
86
+ ```bash
87
+ pip install universal-host-manager-mcp
88
+ ```
89
+
90
+ Or with [uv](https://docs.astral.sh/uv/) / [pipx](https://pipx.pypa.io/), without polluting a project environment:
91
+
92
+ ```bash
93
+ uvx universal-host-manager-mcp
94
+ ```
95
+
96
+ From source (for development):
97
+
98
+ ```bash
99
+ python -m venv .venv
100
+ source .venv/bin/activate
101
+ pip install -e .
102
+ ```
103
+
104
+ ## Run
105
+
106
+ ```bash
107
+ universal-host-manager-mcp
108
+ ```
109
+
110
+ (Running from a source checkout with the `.venv` activated works the same way — the console script is installed by `pip install -e .`.)
111
+
112
+ With the default port, the Streamable HTTP endpoint is:
113
+
114
+ ```text
115
+ http://127.0.0.1:8765/mcp
116
+ ```
117
+
118
+ For an intentional local-only test without Auth0:
119
+
120
+ ```bash
121
+ ALLOW_INSECURE_NO_AUTH=true universal-host-manager-mcp
122
+ ```
123
+
124
+ Do not use insecure mode on a publicly reachable endpoint.
125
+
126
+ ## Cloudflare Tunnel
127
+
128
+ Install `cloudflared`, authenticate it and create a named tunnel:
129
+
130
+ ```bash
131
+ cloudflared tunnel login
132
+ cloudflared tunnel create universal-host-manager-mcp
133
+ cloudflared tunnel route dns universal-host-manager-mcp mcp.example.com
134
+ ```
135
+
136
+ Create `~/.cloudflared/config.yml`:
137
+
138
+ ```yaml
139
+ tunnel: YOUR_TUNNEL_ID
140
+ credentials-file: /home/youruser/.cloudflared/YOUR_TUNNEL_ID.json
141
+
142
+ ingress:
143
+ - hostname: mcp.example.com
144
+ service: http://127.0.0.1:8765
145
+ - service: http_status:404
146
+ ```
147
+
148
+ Validate and run it:
149
+
150
+ ```bash
151
+ cloudflared tunnel ingress validate
152
+ cloudflared tunnel run universal-host-manager-mcp
153
+ ```
154
+
155
+ Your remote MCP URL will be:
156
+
157
+ ```text
158
+ https://mcp.example.com/mcp
159
+ ```
160
+
161
+ Set `MCP_BASE_URL=https://mcp.example.com`; do not include `/mcp` in `MCP_BASE_URL`.
162
+
163
+ ## ChatGPT and Claude
164
+
165
+ Add the public Streamable HTTP URL to the client's MCP/connector configuration:
166
+
167
+ ```text
168
+ https://mcp.example.com/mcp
169
+ ```
170
+
171
+ Complete the Auth0 sign-in when the client opens the authorization flow. The exact settings screens and supported connector options can change, so follow the current client documentation rather than using legacy SSE instructions.
172
+
173
+ Multiple clients can connect to the same running HTTP server. Each client authenticates independently; no separate server process or port is required.
174
+
175
+ ## Background service
176
+
177
+ ### Linux systemd
178
+
179
+ Copy and edit the included unit:
180
+
181
+ ```bash
182
+ sudo cp examples/mcp-manager.service /etc/systemd/system/
183
+ sudo systemctl daemon-reload
184
+ sudo systemctl enable --now mcp-manager
185
+ sudo systemctl status mcp-manager --no-pager
186
+ ```
187
+
188
+ The example uses systemd hardening directives. Adjust `ReadWritePaths`, `ProtectHome`, the user, paths and permissions to match the resources the MCP server genuinely needs.
189
+
190
+ ### macOS launchd
191
+
192
+ Edit paths in `examples/com.user.mcpmanager.plist`, then:
193
+
194
+ ```bash
195
+ cp examples/com.user.mcpmanager.plist ~/Library/LaunchAgents/
196
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.mcpmanager.plist
197
+ launchctl kickstart -k gui/$(id -u)/com.user.mcpmanager
198
+ ```
199
+
200
+ ## Configuration
201
+
202
+ | Variable | Default | Description |
203
+ | --- | --- | --- |
204
+ | `HOST` | `127.0.0.1` | Listen address |
205
+ | `PORT` | `8765` | Listen port |
206
+ | `MCP_BASE_URL` | local URL | Public OAuth base URL, without `/mcp` |
207
+ | `MCP_WORKSPACE_DIR` | user home | Boundary for file tools and command working directory |
208
+ | `MAX_OUTPUT_CHARS` | `64000` | Maximum returned tool-output characters |
209
+ | `DEFAULT_CMD_TIMEOUT` | `300` | Default command timeout in seconds |
210
+ | `MAX_CMD_TIMEOUT` | `1800` | Maximum accepted command timeout |
211
+ | `MAX_READ_BYTES` | `5000000` | Maximum file size read by `read_file` |
212
+ | `MAX_WRITE_BYTES` | `5000000` | Maximum content size written by `write_file` |
213
+ | `LOG_LEVEL` | `INFO` | Python log level |
214
+ | `ALLOW_INSECURE_NO_AUTH` | `false` | Explicit local-development authentication bypass |
215
+
216
+ ## Download
217
+
218
+ Clone with Git:
219
+
220
+ ```bash
221
+ git clone https://github.com/Abktya/universal-host-manager-mcp.git
222
+ ```
223
+
224
+ Or use **Code → Download ZIP** on GitHub.
225
+
226
+ ## License
227
+
228
+ [MIT](LICENSE)
@@ -0,0 +1,28 @@
1
+ # Security policy
2
+
3
+ ## Important security model
4
+
5
+ This server intentionally exposes arbitrary shell execution and file-writing
6
+ capabilities. Authentication controls who can call the tools; it does not make
7
+ shell commands safe.
8
+
9
+ `MCP_WORKSPACE_DIR` constrains the built-in file tools and sets the command
10
+ working directory. It is **not** an operating-system sandbox for `run_command`.
11
+ A command can use absolute paths or access any resource permitted to the OS user.
12
+
13
+ ## Safe deployment checklist
14
+
15
+ - Run the service as a dedicated, unprivileged OS user.
16
+ - Grant that user access only to the directories and services it must manage.
17
+ - Set `MCP_WORKSPACE_DIR` explicitly.
18
+ - Keep Auth0 enabled on every remotely reachable deployment.
19
+ - Bind to `127.0.0.1` behind a TLS reverse proxy or Cloudflare Tunnel.
20
+ - Never commit `.env`, tokens, client secrets, SSH keys or tunnel credentials.
21
+ - Do not grant passwordless unrestricted `sudo`.
22
+ - Review logs and rotate credentials if an MCP client or account is compromised.
23
+ - For stronger isolation, run the service inside a locked-down container or VM.
24
+
25
+ ## Reporting a vulnerability
26
+
27
+ Please do not disclose credentials or exploitable deployment details in a public
28
+ issue. Contact the repository owner privately through GitHub.
@@ -0,0 +1,27 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.user.mcpmanager</string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string>/path/to/universal-host-manager-mcp/.venv/bin/universal-host-manager-mcp</string>
10
+ </array>
11
+ <key>WorkingDirectory</key>
12
+ <string>/path/to/universal-host-manager-mcp</string>
13
+ <key>EnvironmentVariables</key>
14
+ <dict>
15
+ <key>PATH</key>
16
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
17
+ </dict>
18
+ <key>StandardOutPath</key>
19
+ <string>/tmp/universal-host-manager-mcp.log</string>
20
+ <key>StandardErrorPath</key>
21
+ <string>/tmp/universal-host-manager-mcp.error.log</string>
22
+ <key>KeepAlive</key>
23
+ <true/>
24
+ <key>RunAtLoad</key>
25
+ <true/>
26
+ </dict>
27
+ </plist>
@@ -0,0 +1,22 @@
1
+ [Unit]
2
+ Description=Universal Host Manager MCP Server
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ User=youruser
9
+ Group=youruser
10
+ WorkingDirectory=/opt/universal-host-manager-mcp
11
+ EnvironmentFile=/opt/universal-host-manager-mcp/.env
12
+ ExecStart=/opt/universal-host-manager-mcp/.venv/bin/universal-host-manager-mcp
13
+ Restart=on-failure
14
+ RestartSec=5
15
+ NoNewPrivileges=true
16
+ PrivateTmp=true
17
+ ProtectSystem=strict
18
+ ProtectHome=read-only
19
+ ReadWritePaths=/home/youruser/workspace
20
+
21
+ [Install]
22
+ WantedBy=multi-user.target
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "universal-host-manager-mcp"
7
+ version = "0.1.0"
8
+ description = "Cross-platform FastMCP server for authenticated remote Linux/macOS host administration"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Ahmet Baktiaya" },
14
+ ]
15
+ keywords = [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "claude",
19
+ "chatgpt",
20
+ "remote-shell",
21
+ "devops",
22
+ "sysadmin",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Intended Audience :: System Administrators",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Operating System :: POSIX :: Linux",
29
+ "Operating System :: MacOS",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Topic :: System :: Systems Administration",
34
+ ]
35
+ dependencies = [
36
+ "fastmcp>=2.13,<5",
37
+ "python-dotenv>=1.0,<2",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/Abktya/universal-host-manager-mcp"
42
+ Repository = "https://github.com/Abktya/universal-host-manager-mcp"
43
+ Issues = "https://github.com/Abktya/universal-host-manager-mcp/issues"
44
+ Security = "https://github.com/Abktya/universal-host-manager-mcp/blob/main/SECURITY.md"
45
+
46
+ [project.scripts]
47
+ universal-host-manager-mcp = "universal_host_manager_mcp:main"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/universal_host_manager_mcp"]
@@ -0,0 +1,2 @@
1
+ fastmcp>=2.13,<5
2
+ python-dotenv>=1.0,<2
@@ -0,0 +1,5 @@
1
+ """Universal Host Manager MCP Server package."""
2
+
3
+ from .server import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,259 @@
1
+ """Universal Host Manager MCP Server.
2
+
3
+ A cross-platform (Linux/macOS) FastMCP server for authenticated remote host
4
+ administration. This server exposes powerful shell and file-management tools;
5
+ read SECURITY.md and run it only as an unprivileged, dedicated OS user.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ import platform
13
+ import signal
14
+ import subprocess
15
+ from functools import wraps
16
+ from pathlib import Path
17
+ from typing import Callable, ParamSpec, TypeVar
18
+
19
+ from dotenv import load_dotenv
20
+ from fastmcp import FastMCP
21
+ from fastmcp.server.auth.providers.auth0 import Auth0Provider
22
+
23
+ load_dotenv()
24
+
25
+ logging.basicConfig(
26
+ level=os.getenv("LOG_LEVEL", "INFO").upper(),
27
+ format="%(asctime)s [%(levelname)s] %(message)s",
28
+ datefmt="%Y-%m-%d %H:%M:%S",
29
+ )
30
+ logger = logging.getLogger("universal-host-manager")
31
+
32
+ HOST = os.getenv("HOST", "127.0.0.1")
33
+ PORT = int(os.getenv("PORT", "8765"))
34
+ BASE_URL = os.getenv("MCP_BASE_URL", f"http://localhost:{PORT}").rstrip("/")
35
+ WORKSPACE_ROOT = Path(os.getenv("MCP_WORKSPACE_DIR", Path.home())).expanduser().resolve()
36
+
37
+ MAX_OUTPUT_CHARS = int(os.getenv("MAX_OUTPUT_CHARS", "64000"))
38
+ DEFAULT_CMD_TIMEOUT = int(os.getenv("DEFAULT_CMD_TIMEOUT", "300"))
39
+ MAX_CMD_TIMEOUT = int(os.getenv("MAX_CMD_TIMEOUT", "1800"))
40
+ MAX_READ_BYTES = int(os.getenv("MAX_READ_BYTES", "5000000"))
41
+ MAX_WRITE_BYTES = int(os.getenv("MAX_WRITE_BYTES", "5000000"))
42
+ ALLOW_INSECURE_NO_AUTH = os.getenv("ALLOW_INSECURE_NO_AUTH", "false").lower() in {
43
+ "1", "true", "yes",
44
+ }
45
+
46
+ AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN")
47
+ AUTH0_CLIENT_ID = os.getenv("AUTH0_CLIENT_ID")
48
+ AUTH0_CLIENT_SECRET = os.getenv("AUTH0_CLIENT_SECRET")
49
+ AUTH0_AUDIENCE = os.getenv("AUTH0_AUDIENCE")
50
+
51
+ auth_values = [AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_AUDIENCE]
52
+ if all(auth_values):
53
+ auth = Auth0Provider(
54
+ config_url=f"https://{AUTH0_DOMAIN}/.well-known/openid-configuration",
55
+ client_id=AUTH0_CLIENT_ID,
56
+ client_secret=AUTH0_CLIENT_SECRET,
57
+ audience=AUTH0_AUDIENCE,
58
+ base_url=BASE_URL,
59
+ allowed_client_redirect_uris=[
60
+ "https://claude.ai/api/mcp/auth_callback",
61
+ "https://claude.com/api/mcp/auth_callback",
62
+ "https://chatgpt.com/connector/oauth/*",
63
+ "https://antigravity.google/oauth-callback",
64
+ "http://localhost:*",
65
+ "http://127.0.0.1:*",
66
+ ],
67
+ )
68
+ elif any(auth_values):
69
+ raise RuntimeError(
70
+ "Incomplete Auth0 configuration. Set AUTH0_DOMAIN, AUTH0_CLIENT_ID, "
71
+ "AUTH0_CLIENT_SECRET and AUTH0_AUDIENCE."
72
+ )
73
+ elif ALLOW_INSECURE_NO_AUTH:
74
+ auth = None
75
+ logger.warning("Authentication is disabled by ALLOW_INSECURE_NO_AUTH=true.")
76
+ else:
77
+ raise RuntimeError(
78
+ "Auth0 configuration is missing. Refusing to start without authentication. "
79
+ "For local-only development, explicitly set ALLOW_INSECURE_NO_AUTH=true."
80
+ )
81
+
82
+ mcp = FastMCP("Universal Host Manager", auth=auth)
83
+
84
+ P = ParamSpec("P")
85
+ R = TypeVar("R")
86
+
87
+
88
+ def _truncate(text: str, limit: int = MAX_OUTPUT_CHARS) -> str:
89
+ if len(text) <= limit:
90
+ return text
91
+ half = limit // 2
92
+ omitted = len(text) - (half * 2)
93
+ return f"{text[:half]}\n\n... [truncated {omitted} chars] ...\n\n{text[-half:]}"
94
+
95
+
96
+ def _validate_path(target_path: str) -> Path:
97
+ candidate = Path(target_path).expanduser()
98
+ if not candidate.is_absolute():
99
+ candidate = WORKSPACE_ROOT / candidate
100
+ resolved = candidate.resolve()
101
+ if resolved != WORKSPACE_ROOT and WORKSPACE_ROOT not in resolved.parents:
102
+ raise PermissionError(f"Path must be inside {WORKSPACE_ROOT}")
103
+ return resolved
104
+
105
+
106
+ def _run(command: str, timeout: int = DEFAULT_CMD_TIMEOUT) -> str:
107
+ """Run ``command`` in a shell, killing the whole process group on timeout.
108
+
109
+ ``subprocess.run(..., shell=True)`` only terminates the immediate shell
110
+ process on timeout; any children it spawned (background jobs, piped
111
+ processes, ``nohup``'d commands, etc.) are left running. Starting the
112
+ shell in its own session (``start_new_session=True``) lets us send the
113
+ kill signal to the whole process group via ``os.killpg`` instead.
114
+ """
115
+ timeout = max(1, min(timeout, MAX_CMD_TIMEOUT))
116
+ process = subprocess.Popen(
117
+ command,
118
+ shell=True,
119
+ stdout=subprocess.PIPE,
120
+ stderr=subprocess.PIPE,
121
+ text=True,
122
+ cwd=WORKSPACE_ROOT,
123
+ errors="replace",
124
+ start_new_session=True, # own process group -> can kill children too
125
+ )
126
+ try:
127
+ stdout, stderr = process.communicate(timeout=timeout)
128
+ except subprocess.TimeoutExpired:
129
+ try:
130
+ os.killpg(process.pid, signal.SIGKILL)
131
+ except ProcessLookupError:
132
+ pass
133
+ stdout, stderr = process.communicate()
134
+ return _truncate(
135
+ f"[ERROR] Command timed out after {timeout}s and was killed "
136
+ f"(including any child processes)\n"
137
+ f"--- partial output ---\n{(stdout + stderr).strip() or '(none)'}"
138
+ )
139
+ except Exception as exc:
140
+ try:
141
+ os.killpg(process.pid, signal.SIGKILL)
142
+ except ProcessLookupError:
143
+ pass
144
+ logger.exception("Command execution failed")
145
+ return f"[ERROR] Execution failed: {type(exc).__name__}: {exc}"
146
+
147
+ output = (stdout + stderr).strip() or "(no output)"
148
+ if process.returncode:
149
+ output = f"[exit={process.returncode}]\n{output}"
150
+ return _truncate(output)
151
+
152
+
153
+ def safe_tool(fn: Callable[P, R]) -> Callable[P, R | str]:
154
+ @wraps(fn)
155
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | str:
156
+ try:
157
+ return fn(*args, **kwargs)
158
+ except Exception as exc:
159
+ logger.exception("Tool execution failed: %s", fn.__name__)
160
+ return f"[ERROR] {fn.__name__} failed: {type(exc).__name__}: {exc}"
161
+
162
+ return wrapper
163
+
164
+
165
+ @mcp.tool()
166
+ @safe_tool
167
+ def run_command(command: str, timeout: int = DEFAULT_CMD_TIMEOUT) -> str:
168
+ """Run an arbitrary shell command with WORKSPACE_ROOT as its working directory.
169
+
170
+ Warning: the working directory is not an OS sandbox. Absolute paths, shell
171
+ redirection and invoked programs may access anything permitted to the server's
172
+ OS user.
173
+ """
174
+ logger.info("[TOOL] run_command timeout=%ss command=%r", timeout, command[:150])
175
+ return _run(command, timeout)
176
+
177
+
178
+ @mcp.tool()
179
+ @safe_tool
180
+ def read_file(path: str) -> str:
181
+ """Read a text file located inside the configured workspace."""
182
+ target = _validate_path(path)
183
+ if not target.exists():
184
+ return f"[ERROR] File not found: {path}"
185
+ if not target.is_file():
186
+ return f"[ERROR] Not a regular file: {path}"
187
+ if target.stat().st_size > MAX_READ_BYTES:
188
+ return f"[ERROR] File exceeds MAX_READ_BYTES ({MAX_READ_BYTES})."
189
+
190
+ raw = target.read_bytes()
191
+ try:
192
+ text = raw.decode("utf-8")
193
+ except UnicodeDecodeError:
194
+ text = raw.decode("latin-1", errors="replace")
195
+ return _truncate(text)
196
+
197
+
198
+ @mcp.tool()
199
+ @safe_tool
200
+ def write_file(path: str, content: str) -> str:
201
+ """Write UTF-8 text inside the configured workspace."""
202
+ encoded = content.encode("utf-8")
203
+ if len(encoded) > MAX_WRITE_BYTES:
204
+ return f"[ERROR] Content exceeds MAX_WRITE_BYTES ({MAX_WRITE_BYTES})."
205
+ target = _validate_path(path)
206
+ target.parent.mkdir(parents=True, exist_ok=True)
207
+ target.write_text(content, encoding="utf-8")
208
+ return f"Wrote {len(encoded)} bytes to {target}"
209
+
210
+
211
+ @mcp.tool()
212
+ @safe_tool
213
+ def list_dir(path: str = ".") -> str:
214
+ """List a directory located inside the configured workspace."""
215
+ target = _validate_path(path)
216
+ if not target.exists():
217
+ return f"[ERROR] Path not found: {path}"
218
+ if not target.is_dir():
219
+ return f"[ERROR] Not a directory: {path}"
220
+ entries = sorted(item.name + ("/" if item.is_dir() else "") for item in target.iterdir())
221
+ return _truncate("\n".join(entries) or "(empty directory)")
222
+
223
+
224
+ @mcp.tool()
225
+ @safe_tool
226
+ def system_metrics() -> str:
227
+ """Return disk, memory and top-process information for Linux or macOS."""
228
+ current_os = platform.system().lower()
229
+ disk = _run("df -h .")
230
+ if current_os == "darwin":
231
+ memory = _run("vm_stat | head -10")
232
+ processes = _run("ps -A -o %cpu,%mem,comm | sort -nr | head -n 10")
233
+ elif current_os == "linux":
234
+ memory = _run("free -h")
235
+ processes = _run("ps aux --sort=-%mem | head -15")
236
+ else:
237
+ return f"[ERROR] Unsupported operating system: {platform.system()}"
238
+
239
+ return (
240
+ f"--- DISK USAGE ---\n{disk}\n\n"
241
+ f"--- MEMORY STATUS ---\n{memory}\n\n"
242
+ f"--- TOP PROCESSES ---\n{processes}"
243
+ )
244
+
245
+
246
+ def main() -> None:
247
+ """Entry point used by the ``universal-host-manager-mcp`` console script."""
248
+ logger.info(
249
+ "Starting Universal Host Manager on %s:%s (OS=%s, workspace=%s)",
250
+ HOST,
251
+ PORT,
252
+ platform.system(),
253
+ WORKSPACE_ROOT,
254
+ )
255
+ mcp.run(transport="streamable-http", host=HOST, port=PORT)
256
+
257
+
258
+ if __name__ == "__main__":
259
+ main()