ssh-agent-workspace 1.0.5 → 1.0.6

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 (158) hide show
  1. package/README.md +471 -471
  2. package/dist/__tests__/SSHManager.test.js +59 -37
  3. package/dist/__tests__/SSHManager.test.js.map +1 -1
  4. package/dist/__tests__/SessionManager.test.js +53 -51
  5. package/dist/__tests__/SessionManager.test.js.map +1 -1
  6. package/dist/__tests__/StorageManager.test.js +74 -74
  7. package/dist/__tests__/StorageManager.test.js.map +1 -1
  8. package/dist/__tests__/ansi.test.js +33 -33
  9. package/dist/__tests__/ansi.test.js.map +1 -1
  10. package/dist/__tests__/security.test.js +54 -54
  11. package/dist/__tests__/security.test.js.map +1 -1
  12. package/dist/__tests__/validation.test.js +17 -17
  13. package/dist/__tests__/validation.test.js.map +1 -1
  14. package/dist/core/HostSecurityManager.d.ts.map +1 -1
  15. package/dist/core/HostSecurityManager.js +9 -9
  16. package/dist/core/HostSecurityManager.js.map +1 -1
  17. package/dist/core/SSHManager.d.ts +1 -1
  18. package/dist/core/SSHManager.d.ts.map +1 -1
  19. package/dist/core/SSHManager.js +31 -31
  20. package/dist/core/SSHManager.js.map +1 -1
  21. package/dist/core/SessionManager.d.ts +4 -4
  22. package/dist/core/SessionManager.d.ts.map +1 -1
  23. package/dist/core/SessionManager.js +14 -14
  24. package/dist/core/SessionManager.js.map +1 -1
  25. package/dist/core/StorageManager.d.ts.map +1 -1
  26. package/dist/core/StorageManager.js +11 -11
  27. package/dist/core/StorageManager.js.map +1 -1
  28. package/dist/core/TmuxManager.d.ts +3 -3
  29. package/dist/core/TmuxManager.d.ts.map +1 -1
  30. package/dist/core/TmuxManager.js +16 -16
  31. package/dist/core/TmuxManager.js.map +1 -1
  32. package/dist/core/ToolConfigManager.d.ts.map +1 -1
  33. package/dist/core/ToolConfigManager.js +10 -10
  34. package/dist/core/ToolConfigManager.js.map +1 -1
  35. package/dist/index.js +35 -35
  36. package/dist/index.js.map +1 -1
  37. package/dist/server.d.ts +4 -4
  38. package/dist/server.d.ts.map +1 -1
  39. package/dist/server.js +41 -41
  40. package/dist/server.js.map +1 -1
  41. package/dist/tools/backup.d.ts +3 -3
  42. package/dist/tools/backup.d.ts.map +1 -1
  43. package/dist/tools/backup.js +86 -49
  44. package/dist/tools/backup.js.map +1 -1
  45. package/dist/tools/connect.d.ts +4 -4
  46. package/dist/tools/connect.d.ts.map +1 -1
  47. package/dist/tools/connect.js +54 -42
  48. package/dist/tools/connect.js.map +1 -1
  49. package/dist/tools/connection_status.d.ts +4 -4
  50. package/dist/tools/connection_status.d.ts.map +1 -1
  51. package/dist/tools/connection_status.js +23 -16
  52. package/dist/tools/connection_status.js.map +1 -1
  53. package/dist/tools/db_query.d.ts +3 -3
  54. package/dist/tools/db_query.d.ts.map +1 -1
  55. package/dist/tools/db_query.js +130 -78
  56. package/dist/tools/db_query.js.map +1 -1
  57. package/dist/tools/deploy.d.ts +3 -3
  58. package/dist/tools/deploy.d.ts.map +1 -1
  59. package/dist/tools/deploy.js +103 -53
  60. package/dist/tools/deploy.js.map +1 -1
  61. package/dist/tools/disconnect.d.ts +3 -3
  62. package/dist/tools/disconnect.d.ts.map +1 -1
  63. package/dist/tools/disconnect.js +16 -19
  64. package/dist/tools/disconnect.js.map +1 -1
  65. package/dist/tools/exec.d.ts +3 -3
  66. package/dist/tools/exec.d.ts.map +1 -1
  67. package/dist/tools/exec.js +40 -45
  68. package/dist/tools/exec.js.map +1 -1
  69. package/dist/tools/group_exec.d.ts +3 -3
  70. package/dist/tools/group_exec.d.ts.map +1 -1
  71. package/dist/tools/group_exec.js +60 -44
  72. package/dist/tools/group_exec.js.map +1 -1
  73. package/dist/tools/health_check.d.ts +3 -3
  74. package/dist/tools/health_check.d.ts.map +1 -1
  75. package/dist/tools/health_check.js +27 -23
  76. package/dist/tools/health_check.js.map +1 -1
  77. package/dist/tools/host_security.d.ts +1 -1
  78. package/dist/tools/host_security.d.ts.map +1 -1
  79. package/dist/tools/host_security.js +30 -36
  80. package/dist/tools/host_security.js.map +1 -1
  81. package/dist/tools/index.d.ts +23 -23
  82. package/dist/tools/index.d.ts.map +1 -1
  83. package/dist/tools/index.js +23 -23
  84. package/dist/tools/index.js.map +1 -1
  85. package/dist/tools/interrupt.d.ts +3 -3
  86. package/dist/tools/interrupt.d.ts.map +1 -1
  87. package/dist/tools/interrupt.js +18 -18
  88. package/dist/tools/interrupt.js.map +1 -1
  89. package/dist/tools/list_hosts.d.ts.map +1 -1
  90. package/dist/tools/list_hosts.js +5 -7
  91. package/dist/tools/list_hosts.js.map +1 -1
  92. package/dist/tools/list_sessions.d.ts +1 -1
  93. package/dist/tools/list_sessions.js +4 -4
  94. package/dist/tools/list_sessions.js.map +1 -1
  95. package/dist/tools/read_output.d.ts +3 -3
  96. package/dist/tools/read_output.d.ts.map +1 -1
  97. package/dist/tools/read_output.js +17 -17
  98. package/dist/tools/read_output.js.map +1 -1
  99. package/dist/tools/reconnect_to_tmux.d.ts +4 -4
  100. package/dist/tools/reconnect_to_tmux.d.ts.map +1 -1
  101. package/dist/tools/reconnect_to_tmux.js +44 -37
  102. package/dist/tools/reconnect_to_tmux.js.map +1 -1
  103. package/dist/tools/send_input.d.ts +3 -3
  104. package/dist/tools/send_input.d.ts.map +1 -1
  105. package/dist/tools/send_input.js +18 -18
  106. package/dist/tools/send_input.js.map +1 -1
  107. package/dist/tools/sftp_download.d.ts +3 -3
  108. package/dist/tools/sftp_download.d.ts.map +1 -1
  109. package/dist/tools/sftp_download.js +41 -27
  110. package/dist/tools/sftp_download.js.map +1 -1
  111. package/dist/tools/sftp_list.d.ts +3 -3
  112. package/dist/tools/sftp_list.d.ts.map +1 -1
  113. package/dist/tools/sftp_list.js +35 -26
  114. package/dist/tools/sftp_list.js.map +1 -1
  115. package/dist/tools/sftp_upload.d.ts +3 -3
  116. package/dist/tools/sftp_upload.d.ts.map +1 -1
  117. package/dist/tools/sftp_upload.js +43 -29
  118. package/dist/tools/sftp_upload.js.map +1 -1
  119. package/dist/tools/ssh_tunnel.d.ts +3 -3
  120. package/dist/tools/ssh_tunnel.d.ts.map +1 -1
  121. package/dist/tools/ssh_tunnel.js +130 -76
  122. package/dist/tools/ssh_tunnel.js.map +1 -1
  123. package/dist/tools/sync.d.ts +3 -3
  124. package/dist/tools/sync.d.ts.map +1 -1
  125. package/dist/tools/sync.js +103 -61
  126. package/dist/tools/sync.js.map +1 -1
  127. package/dist/tools/tail_log.d.ts +3 -3
  128. package/dist/tools/tail_log.d.ts.map +1 -1
  129. package/dist/tools/tail_log.js +38 -26
  130. package/dist/tools/tail_log.js.map +1 -1
  131. package/dist/tools/tools_config.d.ts +1 -1
  132. package/dist/tools/tools_config.d.ts.map +1 -1
  133. package/dist/tools/tools_config.js +25 -35
  134. package/dist/tools/tools_config.js.map +1 -1
  135. package/dist/types/index.d.ts +1 -1
  136. package/dist/types/index.d.ts.map +1 -1
  137. package/dist/utils/ansi.js +1 -1
  138. package/dist/utils/ansi.js.map +1 -1
  139. package/dist/utils/logger.d.ts +1 -1
  140. package/dist/utils/logger.js +2 -2
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/security.d.ts +1 -1
  143. package/dist/utils/security.js +3 -3
  144. package/dist/utils/security.js.map +1 -1
  145. package/dist/utils/ssh.d.ts +2 -2
  146. package/dist/utils/ssh.js +5 -5
  147. package/dist/utils/ssh.js.map +1 -1
  148. package/dist/utils/sshConfig.d.ts +1 -1
  149. package/dist/utils/sshConfig.d.ts.map +1 -1
  150. package/dist/utils/sshConfig.js +21 -21
  151. package/dist/utils/sshConfig.js.map +1 -1
  152. package/dist/utils/validation.d.ts +1 -1
  153. package/dist/utils/validation.js +3 -3
  154. package/dist/utils/validation.js.map +1 -1
  155. package/docs/SECURITY.md +213 -213
  156. package/docs/TOOLS.md +425 -425
  157. package/package.json +59 -48
  158. package/vitest.config.ts +10 -10
package/README.md CHANGED
@@ -1,471 +1,471 @@
1
- # ssh-agent-workspace
2
-
3
- <p align="left">
4
- <img src="https://img.shields.io/badge/Node.js-≥18-339933?logo=node.js" alt="Node.js ≥18">
5
- <img src="https://img.shields.io/badge/MCP-Server-orange" alt="MCP">
6
- <img src="https://img.shields.io/badge/Tools-25-blue" alt="25 tools">
7
- <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT">
8
- <img src="https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey" alt="platform">
9
- </p>
10
-
11
- **Stateful persistent workspace for AI agents over SSH.**
12
-
13
- Unlike traditional SSH MCP servers that execute every command in a fresh shell, SSH Agent Workspace provides a tmux-backed workspace that survives multiple commands, SSH reconnects, MCP restarts, and network interruptions. Your working directory, environment variables, shell history, and running processes remain intact.
14
-
15
- ```
16
- Traditional SSH MCP
17
-
18
- AI
19
- └─ ssh exec channel
20
- └─ command
21
- └─ state lost every time
22
-
23
-
24
- SSH Agent Workspace
25
-
26
- AI
27
- └─ persistent tmux workspace
28
- ├─ cwd persists
29
- ├─ env persists
30
- ├─ shell history persists
31
- ├─ running processes persist
32
- └─ auto recovery
33
- ```
34
-
35
- ---
36
-
37
- ## Why This Exists
38
-
39
- Most SSH MCP servers use exec channels. Every command starts from scratch:
40
-
41
- ```
42
- ❌ No persistent cwd — cd /var/www before every command
43
- ❌ No persistent env vars — re-export forever
44
- ❌ Interactive programs break — vim, htop, docker attach don't work
45
- ❌ State disappears after reconnect — start over from nothing
46
- ```
47
-
48
- SSH Agent Workspace treats SSH as a **persistent workspace** instead of a command runner. Give your AI agent a real terminal that stays alive.
49
-
50
- ---
51
-
52
- ## Core Features
53
-
54
- - **Stateful workspaces** — Persistent tmux-backed sessions. cwd, env, history survive everything.
55
- - **Automatic recovery** — Reconnect to existing sessions after SSH drops or MCP restarts.
56
- - **Runtime reconfiguration** — Enable/disable tools, update per-host security policies without restart.
57
- - **Deterministic output** — Prompt sentinel-based execution. No sleep-based output guessing.
58
-
59
- ---
60
-
61
- ## Quick Start
62
-
63
- ### Install
64
-
65
- ```bash
66
- npm install -g ssh-agent-workspace
67
- ```
68
-
69
- Or from source:
70
-
71
- ```bash
72
- git clone https://github.com/ShiroNexo/ssh-agent-workspace.git
73
- cd ssh-agent-workspace
74
- npm install && npm run build
75
- ```
76
-
77
- ### Setup Your SSH Config
78
-
79
- Hosts must be defined in `~/.ssh/config`:
80
-
81
- ```
82
- Host prod
83
- HostName 10.0.0.5
84
- User deploy
85
- IdentityFile ~/.ssh/id_ed25519
86
-
87
- Host staging
88
- HostName 10.0.0.10
89
- User deploy
90
- IdentityFile ~/.ssh/id_ed25519
91
-
92
- Host internal
93
- HostName 172.16.0.50
94
- User admin
95
- ProxyJump bastion
96
-
97
- Host bastion
98
- HostName jump.example.com
99
- User jumpuser
100
- ```
101
-
102
- ### Try It
103
-
104
- ```
105
- > connect host=prod
106
- → { session_id: "sess_abc", tmux_session: "mcp_prod_x1y2z3" }
107
- ```
108
-
109
- Your agent now has a persistent workspace on `prod`. Running `cd /var/www` once means the agent stays there for every subsequent command.
110
-
111
- ### Add to Your MCP Client
112
-
113
- <details>
114
- <summary><b>OpenCode</b></summary>
115
-
116
- Add to `~/.config/opencode/opencode.json`:
117
-
118
- ```json
119
- {
120
- "$schema": "https://opencode.ai/config.json",
121
- "mcp": {
122
- "workspace": {
123
- "type": "local",
124
- "command": ["npx", "-y", "ssh-agent-workspace"]
125
- }
126
- }
127
- }
128
- ```
129
-
130
- Or via CLI:
131
-
132
- ```bash
133
- opencode mcp add workspace -- npx -y ssh-agent-workspace
134
- ```
135
- </details>
136
-
137
- <details>
138
- <summary><b>Claude Code</b></summary>
139
-
140
- ```bash
141
- claude mcp add workspace -- npx -y ssh-agent-workspace
142
- ```
143
-
144
- Or add to `~/.config/claude-code/claude_code_config.json` or project `.mcp.json`:
145
-
146
- ```json
147
- {
148
- "mcpServers": {
149
- "workspace": {
150
- "command": "npx",
151
- "args": ["-y", "ssh-agent-workspace"],
152
- "autoApprove": [
153
- "mcp__workspace__connect",
154
- "mcp__workspace__exec",
155
- "mcp__workspace__send_input",
156
- "mcp__workspace__read_output",
157
- "mcp__workspace__list_hosts",
158
- "mcp__workspace__list_sessions",
159
- "mcp__workspace__sftp_upload",
160
- "mcp__workspace__sftp_download",
161
- "mcp__workspace__sftp_list"
162
- ]
163
- }
164
- }
165
- }
166
- ```
167
-
168
- > **Tip:** The `autoApprove` block lets the agent use those tools without asking permission each time. Add or remove tools based on your comfort level.
169
- </details>
170
-
171
- <details>
172
- <summary><b>Cursor</b></summary>
173
-
174
- Go to `Cursor Settings` → `MCP` → `New MCP Server`. Use this config:
175
-
176
- ```json
177
- {
178
- "mcpServers": {
179
- "workspace": {
180
- "command": "npx",
181
- "args": ["-y", "ssh-agent-workspace"]
182
- }
183
- }
184
- }
185
- ```
186
- </details>
187
-
188
- <details>
189
- <summary><b>Codex (OpenAI)</b></summary>
190
-
191
- ```bash
192
- codex mcp add workspace -- npx -y ssh-agent-workspace
193
- ```
194
-
195
- Or add to `~/.codex/config.toml`:
196
-
197
- ```toml
198
- [mcp_servers.workspace]
199
- command = "npx"
200
- args = ["-y", "ssh-agent-workspace"]
201
- ```
202
- </details>
203
-
204
- <details>
205
- <summary><b>Windsurf</b></summary>
206
-
207
- Add to `~/.codeium/windsurf/mcp_config.json`:
208
-
209
- ```json
210
- {
211
- "mcpServers": {
212
- "workspace": {
213
- "command": "npx",
214
- "args": ["-y", "ssh-agent-workspace"]
215
- }
216
- }
217
- }
218
- ```
219
- </details>
220
-
221
- <details>
222
- <summary><b>Copilot / VS Code</b></summary>
223
-
224
- ```json
225
- {
226
- "mcpServers": {
227
- "workspace": {
228
- "command": "npx",
229
- "args": ["-y", "ssh-agent-workspace"]
230
- }
231
- }
232
- }
233
- ```
234
- </details>
235
-
236
- <details>
237
- <summary><b>Gemini CLI</b></summary>
238
-
239
- ```bash
240
- gemini mcp add workspace npx -y ssh-agent-workspace
241
- ```
242
- </details>
243
-
244
- <details>
245
- <summary><b>Cline</b></summary>
246
-
247
- ```json
248
- {
249
- "mcpServers": {
250
- "workspace": {
251
- "command": "npx",
252
- "args": ["-y", "ssh-agent-workspace"]
253
- }
254
- }
255
- }
256
- ```
257
- </details>
258
-
259
- <details>
260
- <summary><b>Qoder</b></summary>
261
-
262
- ```bash
263
- qodercli mcp add workspace -- npx -y ssh-agent-workspace
264
- ```
265
- </details>
266
-
267
- > **Using npx** means no global install needed. npx auto-downloads the latest version. If you installed globally (`npm install -g ssh-agent-workspace`), replace `"npx"` / `"-y"` / `"ssh-agent-workspace"` with `"ssh-agent-workspace"` as the command directly.
268
-
269
- ---
270
-
271
- ## Key Features
272
-
273
- ### Workspace Persistence
274
-
275
- Every session is a **dedicated tmux session** on the remote host. Your agent's working directory, environment variables, shell history, and running processes persist across commands, disconnections, and server restarts.
276
-
277
- ```
278
- connect → tmux workspace created with PS1='__MCP_PROMPT__> '
279
-
280
- exec "cd /var/www" → prompt wait → output returned → cwd is now /var/www
281
- exec "docker ps" → runs in /var/www, no need to cd again
282
- exec "vim app.js" → vim opens and stays running in tmux
283
-
284
- (SSH drops, MCP restarts...)
285
-
286
- reconnect_to_tmux → same tmux session, same cwd, vim still open
287
- ```
288
-
289
- ### Deterministic Output
290
-
291
- Instead of `sleep 3 && capture`, every `exec` call polls `tmux capture-pane` until the exact prompt string `__MCP_PROMPT__>` appears. No race conditions, no false positives from output that looks like a prompt.
292
-
293
- ```
294
- Send command → grace interval → poll every 250ms → prompt detected → capture → return output
295
- ```
296
-
297
- ### Auto-Recovery
298
-
299
- On server start, `ssh-agent-workspace` scans all configured hosts for `mcp_*` tmux sessions and automatically reconnects. Disable with `MCP_SSH_RESTORE_SESSIONS=false`.
300
-
301
- ### Runtime Reconfiguration
302
-
303
- | Tool | What it does |
304
- |---|---|
305
- | `tools_config` | Enable/disable any tool at runtime. Persistent. `tools_config` itself can never be disabled. |
306
- | `host_security` | Set per-host read-only, command allowlist/denylist at runtime. Overrides global env vars per host. |
307
-
308
- No restart needed. Changes apply immediately.
309
-
310
- ### Three-Layer Security
311
-
312
- | Layer | Scope | Mechanism |
313
- |---|---|---|
314
- | **Global** | All hosts | `MCP_SSH_READONLY`, `MCP_SSH_ALLOWED_HOSTS`, `MCP_SSH_DENYLIST_COMMANDS` |
315
- | **Per-Host** | Individual host | `host_security` tool: `readonly`, `allow_commands`, `deny_commands` |
316
- | **Per-Operation** | Single command/query | SQL keyword blocklist, path sanitization, shell escaping |
317
-
318
- ---
319
-
320
- ## Tools (25)
321
-
322
- <details>
323
- <summary><b>Workspace (9 tools)</b></summary>
324
-
325
- | Tool | Description |
326
- |---|---|
327
- | `connect` | Create persistent tmux workspace on remote host |
328
- | `reconnect_to_tmux` | Reattach to existing workspace after disconnect |
329
- | `exec` | Run command, wait for prompt, return output |
330
- | `send_input` | Inject raw input (non-blocking) |
331
- | `read_output` | Capture pane tail |
332
- | `interrupt` | Ctrl-C / Ctrl-D signal |
333
- | `disconnect` | Close session. Optionally kill tmux or keep alive |
334
- | `list_hosts` | List `~/.ssh/config` aliases |
335
- | `list_sessions` | List active workspaces |
336
- </details>
337
-
338
- <details>
339
- <summary><b>File Transfer (3 tools)</b></summary>
340
-
341
- | Tool | Description |
342
- |---|---|
343
- | `sftp_upload` | Upload file to remote |
344
- | `sftp_download` | Download file from remote |
345
- | `sftp_list` | List remote directory |
346
- </details>
347
-
348
- <details>
349
- <summary><b>Monitoring (3 tools)</b></summary>
350
-
351
- | Tool | Description |
352
- |---|---|
353
- | `connection_status` | SSH liveness + tmux existence |
354
- | `health_check` | CPU / RAM / Disk / Load / Uptime |
355
- | `tail_log` | Log tail with optional follow |
356
- </details>
357
-
358
- <details>
359
- <summary><b>DevOps (6 tools)</b></summary>
360
-
361
- | Tool | Description |
362
- |---|---|
363
- | `deploy` | Upload → backup → chmod → chown → restart |
364
- | `backup` | tar.gz archive → download → cleanup |
365
- | `sync` | Rsync-lite via SFTP (bidirectional, dry-run) |
366
- | `ssh_tunnel_open` | Local port forward or SOCKS5 proxy |
367
- | `ssh_tunnel_list` | List active tunnels |
368
- | `ssh_tunnel_close` | Close tunnel, free port |
369
- </details>
370
-
371
- <details>
372
- <summary><b>Cluster & Queries (2 tools)</b></summary>
373
-
374
- | Tool | Description |
375
- |---|---|
376
- | `group_exec` | Run command across multiple workspaces (parallel/sequential) |
377
- | `db_query` | Read-only MySQL / PostgreSQL / MongoDB via SSH |
378
- </details>
379
-
380
- <details>
381
- <summary><b>Runtime Config (2 tools)</b></summary>
382
-
383
- | Tool | Description |
384
- |---|---|
385
- | `tools_config` | Enable/disable tools at runtime |
386
- | `host_security` | Per-host read-only, command allow/denylist |
387
- </details>
388
-
389
- **Full reference:** [`docs/TOOLS.md`](docs/TOOLS.md) | **Security:** [`docs/SECURITY.md`](docs/SECURITY.md)
390
-
391
- ---
392
-
393
- ## Usage Examples
394
-
395
- ```
396
- > connect host=prod
397
- → session_id: "sess_abc", tmux_session: "mcp_prod_x1y2z3"
398
-
399
- > exec session_id=sess_abc command="cd /var/www && docker ps"
400
- → { output: "3 containers running" }
401
-
402
- > exec session_id=sess_abc command="ls"
403
- → { output: "..." } (cwd is still /var/www)
404
-
405
- > group_exec session_ids=["sess_abc","sess_def"] command="uptime"
406
- → [{ host: "prod", output: "up 14 days" }, { host: "staging", output: "up 3 days" }]
407
-
408
- > health_check session_id=sess_abc
409
- → { cpu: 12%, memory: 45%, disk: [{ "/": 56% }], uptime: "14 days" }
410
-
411
- > db_query session_id=sess_abc type=mysql database=mydb query="SELECT COUNT(*) FROM users"
412
- → [{ "COUNT(*)": 15423 }]
413
-
414
- > deploy session_id=sess_abc files=[{"local":"dist/app.js","remote":"/var/www/app.js"}] backup=true chmod="755" restart_service="nginx"
415
-
416
- > host_security action=set host=prod readonly=true
417
- → Host 'prod' locked to read-only
418
-
419
- > tools_config disable backup
420
- → Tool 'backup' disabled. Removed from MCP tool list.
421
- ```
422
-
423
- ---
424
-
425
- ## Configuration
426
-
427
- ### Environment Variables
428
-
429
- | Variable | Default | Description |
430
- |---|---|---|
431
- | `LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error` |
432
- | `MCP_SSH_READONLY` | `false` | Block all write operations globally |
433
- | `MCP_SSH_ALLOWED_HOSTS` | `(all)` | Comma-separated host whitelist |
434
- | `MCP_SSH_DENYLIST_COMMANDS` | `(none)` | Global command blocklist |
435
- | `MCP_SSH_RESTORE_SESSIONS` | `true` | Auto-restore workspaces on startup |
436
-
437
- ---
438
-
439
- ## Project Structure
440
-
441
- ```
442
- src/
443
- ├── core/
444
- │ ├── SessionManager.ts # Session registry + persistence
445
- │ ├── SSHManager.ts # SSH2 connections, SFTP, proxy-jump
446
- │ ├── TmuxManager.ts # Tmux operations (create, capture, signal)
447
- │ ├── StorageManager.ts # Persistent session storage (JSON)
448
- │ ├── ToolConfigManager.ts # Runtime tool enable/disable
449
- │ └── HostSecurityManager.ts # Per-host security policies
450
- ├── tools/ # 25 MCP tool handlers
451
- └── utils/ # Security, SSH config parsing, logging, validation
452
- ```
453
-
454
- ---
455
-
456
- ## Troubleshooting
457
-
458
- ```bash
459
- ssh-add -l # Verify SSH agent key
460
- ssh <alias> # Test manual login
461
- chmod 600 ~/.ssh/id_* # Fix key permissions
462
-
463
- # Clean stale workspaces on remote
464
- tmux ls | grep '^mcp_' | awk -F: '{print $1}' | xargs -I{} tmux kill-session -t {}
465
- ```
466
-
467
- ---
468
-
469
- ## License
470
-
471
- MIT
1
+ # ssh-agent-workspace
2
+
3
+ <p align="left">
4
+ <img src="https://img.shields.io/badge/Node.js-≥18-339933?logo=node.js" alt="Node.js ≥18">
5
+ <img src="https://img.shields.io/badge/MCP-Server-orange" alt="MCP">
6
+ <img src="https://img.shields.io/badge/Tools-25-blue" alt="25 tools">
7
+ <img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT">
8
+ <img src="https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey" alt="platform">
9
+ </p>
10
+
11
+ **Stateful persistent workspace for AI agents over SSH.**
12
+
13
+ Unlike traditional SSH MCP servers that execute every command in a fresh shell, SSH Agent Workspace provides a tmux-backed workspace that survives multiple commands, SSH reconnects, MCP restarts, and network interruptions. Your working directory, environment variables, shell history, and running processes remain intact.
14
+
15
+ ```
16
+ Traditional SSH MCP
17
+
18
+ AI
19
+ └─ ssh exec channel
20
+ └─ command
21
+ └─ state lost every time
22
+
23
+
24
+ SSH Agent Workspace
25
+
26
+ AI
27
+ └─ persistent tmux workspace
28
+ ├─ cwd persists
29
+ ├─ env persists
30
+ ├─ shell history persists
31
+ ├─ running processes persist
32
+ └─ auto recovery
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Why This Exists
38
+
39
+ Most SSH MCP servers use exec channels. Every command starts from scratch:
40
+
41
+ ```
42
+ ❌ No persistent cwd — cd /var/www before every command
43
+ ❌ No persistent env vars — re-export forever
44
+ ❌ Interactive programs break — vim, htop, docker attach don't work
45
+ ❌ State disappears after reconnect — start over from nothing
46
+ ```
47
+
48
+ SSH Agent Workspace treats SSH as a **persistent workspace** instead of a command runner. Give your AI agent a real terminal that stays alive.
49
+
50
+ ---
51
+
52
+ ## Core Features
53
+
54
+ - **Stateful workspaces** — Persistent tmux-backed sessions. cwd, env, history survive everything.
55
+ - **Automatic recovery** — Reconnect to existing sessions after SSH drops or MCP restarts.
56
+ - **Runtime reconfiguration** — Enable/disable tools, update per-host security policies without restart.
57
+ - **Deterministic output** — Prompt sentinel-based execution. No sleep-based output guessing.
58
+
59
+ ---
60
+
61
+ ## Quick Start
62
+
63
+ ### Install
64
+
65
+ ```bash
66
+ npm install -g ssh-agent-workspace
67
+ ```
68
+
69
+ Or from source:
70
+
71
+ ```bash
72
+ git clone https://github.com/ShiroNexo/ssh-agent-workspace.git
73
+ cd ssh-agent-workspace
74
+ npm install && npm run build
75
+ ```
76
+
77
+ ### Setup Your SSH Config
78
+
79
+ Hosts must be defined in `~/.ssh/config`:
80
+
81
+ ```
82
+ Host prod
83
+ HostName 10.0.0.5
84
+ User deploy
85
+ IdentityFile ~/.ssh/id_ed25519
86
+
87
+ Host staging
88
+ HostName 10.0.0.10
89
+ User deploy
90
+ IdentityFile ~/.ssh/id_ed25519
91
+
92
+ Host internal
93
+ HostName 172.16.0.50
94
+ User admin
95
+ ProxyJump bastion
96
+
97
+ Host bastion
98
+ HostName jump.example.com
99
+ User jumpuser
100
+ ```
101
+
102
+ ### Try It
103
+
104
+ ```
105
+ > connect host=prod
106
+ → { session_id: "sess_abc", tmux_session: "mcp_prod_x1y2z3" }
107
+ ```
108
+
109
+ Your agent now has a persistent workspace on `prod`. Running `cd /var/www` once means the agent stays there for every subsequent command.
110
+
111
+ ### Add to Your MCP Client
112
+
113
+ <details>
114
+ <summary><b>OpenCode</b></summary>
115
+
116
+ Add to `~/.config/opencode/opencode.json`:
117
+
118
+ ```json
119
+ {
120
+ "$schema": "https://opencode.ai/config.json",
121
+ "mcp": {
122
+ "workspace": {
123
+ "type": "local",
124
+ "command": ["npx", "-y", "ssh-agent-workspace"]
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ Or via CLI:
131
+
132
+ ```bash
133
+ opencode mcp add workspace -- npx -y ssh-agent-workspace
134
+ ```
135
+ </details>
136
+
137
+ <details>
138
+ <summary><b>Claude Code</b></summary>
139
+
140
+ ```bash
141
+ claude mcp add workspace -- npx -y ssh-agent-workspace
142
+ ```
143
+
144
+ Or add to `~/.config/claude-code/claude_code_config.json` or project `.mcp.json`:
145
+
146
+ ```json
147
+ {
148
+ "mcpServers": {
149
+ "workspace": {
150
+ "command": "npx",
151
+ "args": ["-y", "ssh-agent-workspace"],
152
+ "autoApprove": [
153
+ "mcp__workspace__connect",
154
+ "mcp__workspace__exec",
155
+ "mcp__workspace__send_input",
156
+ "mcp__workspace__read_output",
157
+ "mcp__workspace__list_hosts",
158
+ "mcp__workspace__list_sessions",
159
+ "mcp__workspace__sftp_upload",
160
+ "mcp__workspace__sftp_download",
161
+ "mcp__workspace__sftp_list"
162
+ ]
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ > **Tip:** The `autoApprove` block lets the agent use those tools without asking permission each time. Add or remove tools based on your comfort level.
169
+ </details>
170
+
171
+ <details>
172
+ <summary><b>Cursor</b></summary>
173
+
174
+ Go to `Cursor Settings` → `MCP` → `New MCP Server`. Use this config:
175
+
176
+ ```json
177
+ {
178
+ "mcpServers": {
179
+ "workspace": {
180
+ "command": "npx",
181
+ "args": ["-y", "ssh-agent-workspace"]
182
+ }
183
+ }
184
+ }
185
+ ```
186
+ </details>
187
+
188
+ <details>
189
+ <summary><b>Codex (OpenAI)</b></summary>
190
+
191
+ ```bash
192
+ codex mcp add workspace -- npx -y ssh-agent-workspace
193
+ ```
194
+
195
+ Or add to `~/.codex/config.toml`:
196
+
197
+ ```toml
198
+ [mcp_servers.workspace]
199
+ command = "npx"
200
+ args = ["-y", "ssh-agent-workspace"]
201
+ ```
202
+ </details>
203
+
204
+ <details>
205
+ <summary><b>Windsurf</b></summary>
206
+
207
+ Add to `~/.codeium/windsurf/mcp_config.json`:
208
+
209
+ ```json
210
+ {
211
+ "mcpServers": {
212
+ "workspace": {
213
+ "command": "npx",
214
+ "args": ["-y", "ssh-agent-workspace"]
215
+ }
216
+ }
217
+ }
218
+ ```
219
+ </details>
220
+
221
+ <details>
222
+ <summary><b>Copilot / VS Code</b></summary>
223
+
224
+ ```json
225
+ {
226
+ "mcpServers": {
227
+ "workspace": {
228
+ "command": "npx",
229
+ "args": ["-y", "ssh-agent-workspace"]
230
+ }
231
+ }
232
+ }
233
+ ```
234
+ </details>
235
+
236
+ <details>
237
+ <summary><b>Gemini CLI</b></summary>
238
+
239
+ ```bash
240
+ gemini mcp add workspace npx -y ssh-agent-workspace
241
+ ```
242
+ </details>
243
+
244
+ <details>
245
+ <summary><b>Cline</b></summary>
246
+
247
+ ```json
248
+ {
249
+ "mcpServers": {
250
+ "workspace": {
251
+ "command": "npx",
252
+ "args": ["-y", "ssh-agent-workspace"]
253
+ }
254
+ }
255
+ }
256
+ ```
257
+ </details>
258
+
259
+ <details>
260
+ <summary><b>Qoder</b></summary>
261
+
262
+ ```bash
263
+ qodercli mcp add workspace -- npx -y ssh-agent-workspace
264
+ ```
265
+ </details>
266
+
267
+ > **Using npx** means no global install needed. npx auto-downloads the latest version. If you installed globally (`npm install -g ssh-agent-workspace`), replace `"npx"` / `"-y"` / `"ssh-agent-workspace"` with `"ssh-agent-workspace"` as the command directly.
268
+
269
+ ---
270
+
271
+ ## Key Features
272
+
273
+ ### Workspace Persistence
274
+
275
+ Every session is a **dedicated tmux session** on the remote host. Your agent's working directory, environment variables, shell history, and running processes persist across commands, disconnections, and server restarts.
276
+
277
+ ```
278
+ connect → tmux workspace created with PS1='__MCP_PROMPT__> '
279
+
280
+ exec "cd /var/www" → prompt wait → output returned → cwd is now /var/www
281
+ exec "docker ps" → runs in /var/www, no need to cd again
282
+ exec "vim app.js" → vim opens and stays running in tmux
283
+
284
+ (SSH drops, MCP restarts...)
285
+
286
+ reconnect_to_tmux → same tmux session, same cwd, vim still open
287
+ ```
288
+
289
+ ### Deterministic Output
290
+
291
+ Instead of `sleep 3 && capture`, every `exec` call polls `tmux capture-pane` until the exact prompt string `__MCP_PROMPT__>` appears. No race conditions, no false positives from output that looks like a prompt.
292
+
293
+ ```
294
+ Send command → grace interval → poll every 250ms → prompt detected → capture → return output
295
+ ```
296
+
297
+ ### Auto-Recovery
298
+
299
+ On server start, `ssh-agent-workspace` scans all configured hosts for `mcp_*` tmux sessions and automatically reconnects. Disable with `MCP_SSH_RESTORE_SESSIONS=false`.
300
+
301
+ ### Runtime Reconfiguration
302
+
303
+ | Tool | What it does |
304
+ |---|---|
305
+ | `tools_config` | Enable/disable any tool at runtime. Persistent. `tools_config` itself can never be disabled. |
306
+ | `host_security` | Set per-host read-only, command allowlist/denylist at runtime. Overrides global env vars per host. |
307
+
308
+ No restart needed. Changes apply immediately.
309
+
310
+ ### Three-Layer Security
311
+
312
+ | Layer | Scope | Mechanism |
313
+ |---|---|---|
314
+ | **Global** | All hosts | `MCP_SSH_READONLY`, `MCP_SSH_ALLOWED_HOSTS`, `MCP_SSH_DENYLIST_COMMANDS` |
315
+ | **Per-Host** | Individual host | `host_security` tool: `readonly`, `allow_commands`, `deny_commands` |
316
+ | **Per-Operation** | Single command/query | SQL keyword blocklist, path sanitization, shell escaping |
317
+
318
+ ---
319
+
320
+ ## Tools (25)
321
+
322
+ <details>
323
+ <summary><b>Workspace (9 tools)</b></summary>
324
+
325
+ | Tool | Description |
326
+ |---|---|
327
+ | `connect` | Create persistent tmux workspace on remote host |
328
+ | `reconnect_to_tmux` | Reattach to existing workspace after disconnect |
329
+ | `exec` | Run command, wait for prompt, return output |
330
+ | `send_input` | Inject raw input (non-blocking) |
331
+ | `read_output` | Capture pane tail |
332
+ | `interrupt` | Ctrl-C / Ctrl-D signal |
333
+ | `disconnect` | Close session. Optionally kill tmux or keep alive |
334
+ | `list_hosts` | List `~/.ssh/config` aliases |
335
+ | `list_sessions` | List active workspaces |
336
+ </details>
337
+
338
+ <details>
339
+ <summary><b>File Transfer (3 tools)</b></summary>
340
+
341
+ | Tool | Description |
342
+ |---|---|
343
+ | `sftp_upload` | Upload file to remote |
344
+ | `sftp_download` | Download file from remote |
345
+ | `sftp_list` | List remote directory |
346
+ </details>
347
+
348
+ <details>
349
+ <summary><b>Monitoring (3 tools)</b></summary>
350
+
351
+ | Tool | Description |
352
+ |---|---|
353
+ | `connection_status` | SSH liveness + tmux existence |
354
+ | `health_check` | CPU / RAM / Disk / Load / Uptime |
355
+ | `tail_log` | Log tail with optional follow |
356
+ </details>
357
+
358
+ <details>
359
+ <summary><b>DevOps (6 tools)</b></summary>
360
+
361
+ | Tool | Description |
362
+ |---|---|
363
+ | `deploy` | Upload → backup → chmod → chown → restart |
364
+ | `backup` | tar.gz archive → download → cleanup |
365
+ | `sync` | Rsync-lite via SFTP (bidirectional, dry-run) |
366
+ | `ssh_tunnel_open` | Local port forward or SOCKS5 proxy |
367
+ | `ssh_tunnel_list` | List active tunnels |
368
+ | `ssh_tunnel_close` | Close tunnel, free port |
369
+ </details>
370
+
371
+ <details>
372
+ <summary><b>Cluster & Queries (2 tools)</b></summary>
373
+
374
+ | Tool | Description |
375
+ |---|---|
376
+ | `group_exec` | Run command across multiple workspaces (parallel/sequential) |
377
+ | `db_query` | Read-only MySQL / PostgreSQL / MongoDB via SSH |
378
+ </details>
379
+
380
+ <details>
381
+ <summary><b>Runtime Config (2 tools)</b></summary>
382
+
383
+ | Tool | Description |
384
+ |---|---|
385
+ | `tools_config` | Enable/disable tools at runtime |
386
+ | `host_security` | Per-host read-only, command allow/denylist |
387
+ </details>
388
+
389
+ **Full reference:** [`docs/TOOLS.md`](docs/TOOLS.md) | **Security:** [`docs/SECURITY.md`](docs/SECURITY.md)
390
+
391
+ ---
392
+
393
+ ## Usage Examples
394
+
395
+ ```
396
+ > connect host=prod
397
+ → session_id: "sess_abc", tmux_session: "mcp_prod_x1y2z3"
398
+
399
+ > exec session_id=sess_abc command="cd /var/www && docker ps"
400
+ → { output: "3 containers running" }
401
+
402
+ > exec session_id=sess_abc command="ls"
403
+ → { output: "..." } (cwd is still /var/www)
404
+
405
+ > group_exec session_ids=["sess_abc","sess_def"] command="uptime"
406
+ → [{ host: "prod", output: "up 14 days" }, { host: "staging", output: "up 3 days" }]
407
+
408
+ > health_check session_id=sess_abc
409
+ → { cpu: 12%, memory: 45%, disk: [{ "/": 56% }], uptime: "14 days" }
410
+
411
+ > db_query session_id=sess_abc type=mysql database=mydb query="SELECT COUNT(*) FROM users"
412
+ → [{ "COUNT(*)": 15423 }]
413
+
414
+ > deploy session_id=sess_abc files=[{"local":"dist/app.js","remote":"/var/www/app.js"}] backup=true chmod="755" restart_service="nginx"
415
+
416
+ > host_security action=set host=prod readonly=true
417
+ → Host 'prod' locked to read-only
418
+
419
+ > tools_config disable backup
420
+ → Tool 'backup' disabled. Removed from MCP tool list.
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Configuration
426
+
427
+ ### Environment Variables
428
+
429
+ | Variable | Default | Description |
430
+ |---|---|---|
431
+ | `LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error` |
432
+ | `MCP_SSH_READONLY` | `false` | Block all write operations globally |
433
+ | `MCP_SSH_ALLOWED_HOSTS` | `(all)` | Comma-separated host whitelist |
434
+ | `MCP_SSH_DENYLIST_COMMANDS` | `(none)` | Global command blocklist |
435
+ | `MCP_SSH_RESTORE_SESSIONS` | `true` | Auto-restore workspaces on startup |
436
+
437
+ ---
438
+
439
+ ## Project Structure
440
+
441
+ ```
442
+ src/
443
+ ├── core/
444
+ │ ├── SessionManager.ts # Session registry + persistence
445
+ │ ├── SSHManager.ts # SSH2 connections, SFTP, proxy-jump
446
+ │ ├── TmuxManager.ts # Tmux operations (create, capture, signal)
447
+ │ ├── StorageManager.ts # Persistent session storage (JSON)
448
+ │ ├── ToolConfigManager.ts # Runtime tool enable/disable
449
+ │ └── HostSecurityManager.ts # Per-host security policies
450
+ ├── tools/ # 25 MCP tool handlers
451
+ └── utils/ # Security, SSH config parsing, logging, validation
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Troubleshooting
457
+
458
+ ```bash
459
+ ssh-add -l # Verify SSH agent key
460
+ ssh <alias> # Test manual login
461
+ chmod 600 ~/.ssh/id_* # Fix key permissions
462
+
463
+ # Clean stale workspaces on remote
464
+ tmux ls | grep '^mcp_' | awk -F: '{print $1}' | xargs -I{} tmux kill-session -t {}
465
+ ```
466
+
467
+ ---
468
+
469
+ ## License
470
+
471
+ MIT