@thesashadev/ssh-mcp-server 1.0.0
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.
- package/LICENSE +659 -0
- package/README.md +212 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +82 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/dist/ssh-manager.d.ts +12 -0
- package/dist/ssh-manager.d.ts.map +1 -0
- package/dist/ssh-manager.js +183 -0
- package/dist/ssh-manager.js.map +1 -0
- package/dist/tools/download.d.ts +17 -0
- package/dist/tools/download.d.ts.map +1 -0
- package/dist/tools/download.js +207 -0
- package/dist/tools/download.js.map +1 -0
- package/dist/tools/execute.d.ts +13 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/execute.js +195 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/upload.d.ts +17 -0
- package/dist/tools/upload.d.ts.map +1 -0
- package/dist/tools/upload.js +216 -0
- package/dist/tools/upload.js.map +1 -0
- package/dist/utils/output-format.d.ts +26 -0
- package/dist/utils/output-format.d.ts.map +1 -0
- package/dist/utils/output-format.js +117 -0
- package/dist/utils/output-format.js.map +1 -0
- package/dist/utils/path-utils.d.ts +34 -0
- package/dist/utils/path-utils.d.ts.map +1 -0
- package/dist/utils/path-utils.js +87 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/package.json +34 -0
- package/ssh-servers.example.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# ssh-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for executing commands, uploading and downloading files on remote servers via SSH. Optimized for AI agents (Claude Code, Cursor, Windsurf, Antigravity, etc).
|
|
4
|
+
|
|
5
|
+
## ✨ Features
|
|
6
|
+
|
|
7
|
+
- **Command execution** — sync/async modes, timeout, background polling
|
|
8
|
+
- **Reliable file transfers** — 5 automatic fallback strategies (SFTP parallel → SFTP stream → SCP → base64 → chunked)
|
|
9
|
+
- **Multi-server** — easy switching with workspace-based auto-selection
|
|
10
|
+
- **AI-Native output** — ANSI codes stripped, binary detected, control chars removed
|
|
11
|
+
- **Extreme Performance** — Cached sessions, connection pooling, 64-stream parallel transfers
|
|
12
|
+
|
|
13
|
+
## 🛠 Tools
|
|
14
|
+
|
|
15
|
+
| Tool | Description |
|
|
16
|
+
|------|-------------|
|
|
17
|
+
| `ssh_servers` | List configured servers and their workspace bindings |
|
|
18
|
+
| `ssh_execute` | Run a shell command (sync or async with polling) |
|
|
19
|
+
| `ssh_upload` | Upload a local file to remote server |
|
|
20
|
+
| `ssh_download` | Download a remote file to local machine |
|
|
21
|
+
|
|
22
|
+
## 🚀 Quick Start
|
|
23
|
+
|
|
24
|
+
### Option A: Use via npx (Fastest)
|
|
25
|
+
1. Create `ssh-servers.json` in your current folder.
|
|
26
|
+
2. Run directly:
|
|
27
|
+
```bash
|
|
28
|
+
npx -y @thesashadev/ssh-mcp-server
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Option B: Local Build
|
|
32
|
+
1. Clone & Build:
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/TheSashaDev/ssh-mcp-server.git
|
|
35
|
+
cd ssh-mcp-server
|
|
36
|
+
npm install
|
|
37
|
+
npm run build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
2. Create `ssh-servers.json` in the project root.
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"servers": [
|
|
44
|
+
{
|
|
45
|
+
"id": "dev",
|
|
46
|
+
"name": "Dev Server",
|
|
47
|
+
"host": "1.2.3.4",
|
|
48
|
+
"username": "ubuntu",
|
|
49
|
+
"password": "your-password",
|
|
50
|
+
"workspaces": ["D:\\projects\\my-app"]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
*Supports password, private key (`privateKeyPath`), and SSH agent auth.*
|
|
56
|
+
|
|
57
|
+
## 🔌 Client Integration
|
|
58
|
+
|
|
59
|
+
Select your AI tool to see the setup guide:
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary><b>🤖 Claude Code (CLI)</b></summary>
|
|
63
|
+
|
|
64
|
+
Run this command in your terminal:
|
|
65
|
+
```bash
|
|
66
|
+
claude mcp add ssh -- node "D:/ssh mco/dist/index.js"
|
|
67
|
+
```
|
|
68
|
+
Or manually add to `~/.config/claude/mcp_servers.json`:
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"ssh": {
|
|
73
|
+
"command": "node",
|
|
74
|
+
"args": ["D:/ssh mco/dist/index.js"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary><b>🖥️ Claude Desktop</b></summary>
|
|
83
|
+
|
|
84
|
+
Edit your `claude_desktop_config.json`:
|
|
85
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
86
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"ssh": {
|
|
92
|
+
"command": "node",
|
|
93
|
+
"args": ["D:/ssh mco/dist/index.js"]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
</details>
|
|
99
|
+
|
|
100
|
+
<details>
|
|
101
|
+
<summary><b>🖱️ Cursor</b></summary>
|
|
102
|
+
|
|
103
|
+
1. Go to **Settings** > **Cursor Settings** > **Features** > **MCP**.
|
|
104
|
+
2. Click **+ Add New MCP Server**.
|
|
105
|
+
3. Name: `ssh`. Type: `command`.
|
|
106
|
+
4. Command:
|
|
107
|
+
```bash
|
|
108
|
+
node "D:/ssh mco/dist/index.js"
|
|
109
|
+
```
|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
<details>
|
|
113
|
+
<summary><b>🏄 Windsurf</b></summary>
|
|
114
|
+
|
|
115
|
+
Edit `~/.codeium/windsurf/mcp_config.json` (macOS/Linux) or `%USERPROFILE%\.codeium\windsurf\mcp_config.json` (Windows):
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"ssh": {
|
|
121
|
+
"command": "node",
|
|
122
|
+
"args": ["D:/ssh mco/dist/index.js"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
</details>
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary><b>🛡️ Antigravity</b></summary>
|
|
131
|
+
|
|
132
|
+
Add to `mcp_config.json`:
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"ssh": {
|
|
137
|
+
"command": "node",
|
|
138
|
+
"args": ["D:/ssh mco/dist/index.js"]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
<details>
|
|
146
|
+
<summary><b>🧠 Codex</b></summary>
|
|
147
|
+
|
|
148
|
+
Add to `codex.toml`:
|
|
149
|
+
```toml
|
|
150
|
+
[mcp_servers."ssh"]
|
|
151
|
+
command = "node"
|
|
152
|
+
args = ["D:/ssh mco/dist/index.js"]
|
|
153
|
+
enabled = true
|
|
154
|
+
```
|
|
155
|
+
</details>
|
|
156
|
+
|
|
157
|
+
<details>
|
|
158
|
+
<summary><b>🔍 Cody (Sourcegraph)</b></summary>
|
|
159
|
+
|
|
160
|
+
Edit `~/.config/cody/mcp_servers.json` (macOS/Linux) or `%USERPROFILE%\.config\cody\mcp_servers.json` (Windows):
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"mcpServers": {
|
|
164
|
+
"ssh": {
|
|
165
|
+
"command": "node",
|
|
166
|
+
"args": ["D:/ssh mco/dist/index.js"]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
|
+
<details>
|
|
174
|
+
<summary><b>🔁 Continue.dev</b></summary>
|
|
175
|
+
|
|
176
|
+
Add to your `.continue/config.json`:
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"contextProviders": [
|
|
180
|
+
{
|
|
181
|
+
"name": "mcp",
|
|
182
|
+
"params": {
|
|
183
|
+
"mcpServers": {
|
|
184
|
+
"ssh": {
|
|
185
|
+
"command": "node",
|
|
186
|
+
"args": ["D:/ssh mco/dist/index.js"]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
</details>
|
|
195
|
+
|
|
196
|
+
## ⚙️ Server Config
|
|
197
|
+
|
|
198
|
+
| Field | Required | Description |
|
|
199
|
+
|-------|----------|-------------|
|
|
200
|
+
| `id` | yes | ID used in tool calls |
|
|
201
|
+
| `host` | yes | SSH host |
|
|
202
|
+
| `username` | yes | SSH username |
|
|
203
|
+
| `password` | no | Password auth |
|
|
204
|
+
| `privateKeyPath`| no | Path to private key |
|
|
205
|
+
| `workspaces` | no | Local folders for auto-selection |
|
|
206
|
+
|
|
207
|
+
### Workspace Auto-Selection
|
|
208
|
+
When `workspaces` are set (e.g. `["D:\\projects\\my-app"]`), the AI automatically selects the correct server based on your current local directory. No manual `server_id` required!
|
|
209
|
+
|
|
210
|
+
## 📜 License
|
|
211
|
+
|
|
212
|
+
AGPL-3.0
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ServerConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
username: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
privateKeyPath?: string;
|
|
9
|
+
passphrase?: string;
|
|
10
|
+
defaultRemoteDir: string;
|
|
11
|
+
workspaces: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface Config {
|
|
14
|
+
servers: ServerConfig[];
|
|
15
|
+
}
|
|
16
|
+
export declare function loadConfig(): Config;
|
|
17
|
+
export declare function getServer(serverId: string): ServerConfig;
|
|
18
|
+
export declare function findServerByWorkspace(localPath?: string): ServerConfig | null;
|
|
19
|
+
export declare function listServers(): Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
host: string;
|
|
23
|
+
workspaces: string[];
|
|
24
|
+
}>;
|
|
25
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,MAAM;IACnB,OAAO,EAAE,YAAY,EAAE,CAAC;CAC3B;AAOD,wBAAgB,UAAU,IAAI,MAAM,CAyCnC;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAQxD;AAED,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAkB7E;AAED,wBAAgB,WAAW,IAAI,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAQrG"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, normalize } from 'path';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
let cachedConfig = null;
|
|
8
|
+
export function loadConfig() {
|
|
9
|
+
if (cachedConfig)
|
|
10
|
+
return cachedConfig;
|
|
11
|
+
let configPath = process.env.SSH_MCP_CONFIG;
|
|
12
|
+
if (!configPath) {
|
|
13
|
+
const cwdConfig = resolve(process.cwd(), 'ssh-servers.json');
|
|
14
|
+
try {
|
|
15
|
+
if (readFileSync(cwdConfig))
|
|
16
|
+
configPath = cwdConfig;
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
}
|
|
20
|
+
if (!configPath) {
|
|
21
|
+
configPath = resolve(__dirname, '..', 'ssh-servers.json');
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
if (!parsed.servers || !Array.isArray(parsed.servers)) {
|
|
27
|
+
throw new Error('Config must have a "servers" array');
|
|
28
|
+
}
|
|
29
|
+
for (const srv of parsed.servers) {
|
|
30
|
+
if (!srv.id || !srv.host || !srv.username) {
|
|
31
|
+
throw new Error(`Server "${srv.id || 'unknown'}" missing required fields (id, host, username)`);
|
|
32
|
+
}
|
|
33
|
+
srv.port = srv.port || 22;
|
|
34
|
+
srv.defaultRemoteDir = srv.defaultRemoteDir || '/home/' + srv.username;
|
|
35
|
+
srv.workspaces = (srv.workspaces || []).map(w => normalize(w).toLowerCase());
|
|
36
|
+
}
|
|
37
|
+
cachedConfig = parsed;
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.code === 'ENOENT') {
|
|
42
|
+
throw new Error(`Config file not found at ${configPath}. Create ssh-servers.json with your server definitions.`);
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Failed to load config: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function getServer(serverId) {
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
const srv = config.servers.find(s => s.id === serverId);
|
|
50
|
+
if (!srv) {
|
|
51
|
+
const available = config.servers.map(s => `"${s.id}" (${s.name})`).join(', ');
|
|
52
|
+
throw new Error(`Server "${serverId}" not found. Available: ${available}`);
|
|
53
|
+
}
|
|
54
|
+
return srv;
|
|
55
|
+
}
|
|
56
|
+
export function findServerByWorkspace(localPath) {
|
|
57
|
+
if (!localPath)
|
|
58
|
+
return null;
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
const normalizedPath = normalize(localPath).toLowerCase();
|
|
61
|
+
let bestMatch = null;
|
|
62
|
+
let bestLen = 0;
|
|
63
|
+
for (const srv of config.servers) {
|
|
64
|
+
for (const ws of srv.workspaces) {
|
|
65
|
+
if (normalizedPath.startsWith(ws) && ws.length > bestLen) {
|
|
66
|
+
bestMatch = srv;
|
|
67
|
+
bestLen = ws.length;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return bestMatch;
|
|
72
|
+
}
|
|
73
|
+
export function listServers() {
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
return config.servers.map(s => ({
|
|
76
|
+
id: s.id,
|
|
77
|
+
name: s.name,
|
|
78
|
+
host: s.host,
|
|
79
|
+
workspaces: s.workspaces,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAO,MAAM,MAAM,CAAC;AAmB/C,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,IAAI,YAAY,GAAkB,IAAI,CAAC;AAEvC,MAAM,UAAU,UAAU;IACtB,IAAI,YAAY;QAAE,OAAO,YAAY,CAAC;IAEtC,IAAI,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAE5C,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,kBAAkB,CAAC,CAAC;QAC7D,IAAI,CAAC;YACD,IAAI,YAAY,CAAC,SAAS,CAAC;gBAAE,UAAU,GAAG,SAAS,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC,CAAC,CAAC;IACf,CAAC;IAED,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,UAAU,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC;IAED,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAW,CAAC;QAEzC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QAC1D,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;gBACxC,MAAM,IAAI,KAAK,CAAC,WAAW,GAAG,CAAC,EAAE,IAAI,SAAS,gDAAgD,CAAC,CAAC;YACpG,CAAC;YACD,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;YAC1B,GAAG,CAAC,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;YACvE,GAAG,CAAC,UAAU,GAAG,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,YAAY,GAAG,MAAM,CAAC;QACtB,OAAO,MAAM,CAAC;IAClB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAChB,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,4BAA4B,UAAU,yDAAyD,CAAC,CAAC;QACrH,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,0BAA0B,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7D,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,QAAgB;IACtC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,GAAG,EAAE,CAAC;QACP,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9E,MAAM,IAAI,KAAK,CAAC,WAAW,QAAQ,2BAA2B,SAAS,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,SAAkB;IACpD,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,cAAc,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;IAE1D,IAAI,SAAS,GAAwB,IAAI,CAAC;IAC1C,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAC/B,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;YAC9B,IAAI,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;gBACvD,SAAS,GAAG,GAAG,CAAC;gBAChB,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC;YACxB,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO,SAAS,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,WAAW;IACvB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5B,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,UAAU,EAAE,CAAC,CAAC,UAAU;KAC3B,CAAC,CAAC,CAAC;AACR,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { loadConfig, findServerByWorkspace } from './config.js';
|
|
6
|
+
import { executeCommand } from './tools/execute.js';
|
|
7
|
+
import { uploadFile } from './tools/upload.js';
|
|
8
|
+
import { downloadFile } from './tools/download.js';
|
|
9
|
+
import { closeAll } from './ssh-manager.js';
|
|
10
|
+
const server = new McpServer({ name: 'ssh-mcp-server', version: '1.0.0' }, { capabilities: { tools: {}, resources: {} } });
|
|
11
|
+
function resolveServerId(serverId, workspace) {
|
|
12
|
+
if (serverId)
|
|
13
|
+
return serverId;
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
if (workspace) {
|
|
16
|
+
const srv = findServerByWorkspace(workspace);
|
|
17
|
+
if (srv)
|
|
18
|
+
return srv.id;
|
|
19
|
+
}
|
|
20
|
+
if (config.servers.length === 1)
|
|
21
|
+
return config.servers[0].id;
|
|
22
|
+
throw new Error(`Multiple servers configured. Use ssh_servers tool to see available servers, then pass server_id.`);
|
|
23
|
+
}
|
|
24
|
+
// ---- ssh_servers ----
|
|
25
|
+
server.tool('ssh_servers', 'List all configured SSH servers with their IDs, hosts, and workspace bindings. Call this first to discover which server_id to use with other SSH tools.', {}, async () => {
|
|
26
|
+
try {
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
const lines = config.servers.map(s => {
|
|
29
|
+
const ws = s.workspaces.length > 0 ? s.workspaces.join(', ') : 'none';
|
|
30
|
+
return `id=${s.id} | ${s.name} | ${s.host}:${s.port} | user=${s.username} | remote_default=${s.defaultRemoteDir} | workspaces: ${ws}`;
|
|
31
|
+
});
|
|
32
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// ---- ssh_execute ----
|
|
39
|
+
server.tool('ssh_execute', `Run a shell command on a remote SSH server. Output is auto-cleaned (no ANSI, no binary garbage).
|
|
40
|
+
|
|
41
|
+
Sync mode (default): returns stdout, stderr, exit code.
|
|
42
|
+
Async mode (async=true): returns command_id immediately. Call again with command_id to poll status/output.`, {
|
|
43
|
+
server_id: z.string().optional().describe('Server ID. Auto-detected if one server or matched by workspace.'),
|
|
44
|
+
workspace: z.string().optional().describe('Local directory for auto-selecting server.'),
|
|
45
|
+
command: z.string().optional().describe('Shell command. Required unless polling via command_id.'),
|
|
46
|
+
cwd: z.string().optional().describe('Remote working directory.'),
|
|
47
|
+
timeout_ms: z.number().optional().default(30000).describe('Timeout in ms. 0=no limit.'),
|
|
48
|
+
async: z.boolean().optional().default(false).describe('Start in background, return command_id.'),
|
|
49
|
+
command_id: z.string().optional().describe('Poll async command status. Other params ignored when set.'),
|
|
50
|
+
}, async (params) => {
|
|
51
|
+
try {
|
|
52
|
+
if (params.command_id) {
|
|
53
|
+
return { content: [{ type: 'text', text: await executeCommand({ serverId: '', command: '', commandId: params.command_id }) }] };
|
|
54
|
+
}
|
|
55
|
+
if (!params.command) {
|
|
56
|
+
return { content: [{ type: 'text', text: 'Error: command is required unless polling with command_id.' }], isError: true };
|
|
57
|
+
}
|
|
58
|
+
const serverId = resolveServerId(params.server_id, params.workspace);
|
|
59
|
+
const result = await executeCommand({ serverId, command: params.command, cwd: params.cwd, timeoutMs: params.timeout_ms, async: params.async });
|
|
60
|
+
return { content: [{ type: 'text', text: result }] };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// ---- ssh_upload ----
|
|
67
|
+
server.tool('ssh_upload', 'Upload a local file to a remote SSH server. Handles any file type/size. Remote directories created automatically. Paths with spaces are safe.', {
|
|
68
|
+
server_id: z.string().optional().describe('Server ID.'),
|
|
69
|
+
workspace: z.string().optional().describe('Local directory for auto-selecting server.'),
|
|
70
|
+
local_path: z.string().describe('Absolute local file path.'),
|
|
71
|
+
remote_path: z.string().describe('Absolute remote destination path.'),
|
|
72
|
+
overwrite: z.boolean().optional().default(true).describe('Overwrite if exists.'),
|
|
73
|
+
}, async (params) => {
|
|
74
|
+
try {
|
|
75
|
+
const serverId = resolveServerId(params.server_id, params.workspace);
|
|
76
|
+
const result = await uploadFile({ serverId, localPath: params.local_path, remotePath: params.remote_path, overwrite: params.overwrite });
|
|
77
|
+
return { content: [{ type: 'text', text: result }], isError: result.startsWith('FAILED') || result.startsWith('Error') };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// ---- ssh_download ----
|
|
84
|
+
server.tool('ssh_download', 'Download a file from a remote SSH server to the local machine. Handles any file type/size. Local directories created automatically. Paths with spaces are safe.', {
|
|
85
|
+
server_id: z.string().optional().describe('Server ID.'),
|
|
86
|
+
workspace: z.string().optional().describe('Local directory for auto-selecting server.'),
|
|
87
|
+
remote_path: z.string().describe('Absolute remote file path.'),
|
|
88
|
+
local_path: z.string().describe('Absolute local destination path.'),
|
|
89
|
+
overwrite: z.boolean().optional().default(true).describe('Overwrite if exists.'),
|
|
90
|
+
}, async (params) => {
|
|
91
|
+
try {
|
|
92
|
+
const serverId = resolveServerId(params.server_id, params.workspace);
|
|
93
|
+
const result = await downloadFile({ serverId, remotePath: params.remote_path, localPath: params.local_path, overwrite: params.overwrite });
|
|
94
|
+
return { content: [{ type: 'text', text: result }], isError: result.startsWith('FAILED') || result.startsWith('Error') };
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// ---- Start ----
|
|
101
|
+
async function main() {
|
|
102
|
+
try {
|
|
103
|
+
const config = loadConfig();
|
|
104
|
+
console.error(`ssh-mcp: ${config.servers.length} server(s)`);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(`ssh-mcp: ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
const transport = new StdioServerTransport();
|
|
110
|
+
await server.connect(transport);
|
|
111
|
+
console.error('ssh-mcp: ready');
|
|
112
|
+
}
|
|
113
|
+
main().catch((e) => { console.error('Fatal:', e); closeAll(); process.exit(1); });
|
|
114
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Client, type SFTPWrapper } from 'ssh2';
|
|
2
|
+
/**
|
|
3
|
+
* Get or create an SSH connection. Deduplicates concurrent requests for the same server.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getConnection(serverId: string): Promise<Client>;
|
|
6
|
+
/**
|
|
7
|
+
* Get cached SFTP session or create one. Deduplicates concurrent requests.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getSftp(serverId: string): Promise<SFTPWrapper>;
|
|
10
|
+
export declare function closeAll(): void;
|
|
11
|
+
export declare function disconnect(serverId: string): void;
|
|
12
|
+
//# sourceMappingURL=ssh-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-manager.d.ts","sourceRoot":"","sources":["../src/ssh-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAsB,KAAK,WAAW,EAAE,MAAM,MAAM,CAAC;AAoBpE;;GAEG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4ED;;GAEG;AACH,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAkCpE;AAED,wBAAgB,QAAQ,IAAI,IAAI,CAK/B;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMjD"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { getServer } from './config.js';
|
|
4
|
+
const pool = new Map();
|
|
5
|
+
const pendingConnections = new Map(); // Dedup concurrent connect requests
|
|
6
|
+
const CONNECTION_TIMEOUT = 10000;
|
|
7
|
+
const KEEPALIVE_INTERVAL = 15000;
|
|
8
|
+
const IDLE_TIMEOUT = 300000;
|
|
9
|
+
/**
|
|
10
|
+
* Get or create an SSH connection. Deduplicates concurrent requests for the same server.
|
|
11
|
+
*/
|
|
12
|
+
export async function getConnection(serverId) {
|
|
13
|
+
const existing = pool.get(serverId);
|
|
14
|
+
if (existing?.connected) {
|
|
15
|
+
existing.lastUsed = Date.now();
|
|
16
|
+
return existing.client;
|
|
17
|
+
}
|
|
18
|
+
// Dedup: if a connection is already being established, wait for it
|
|
19
|
+
const pending = pendingConnections.get(serverId);
|
|
20
|
+
if (pending)
|
|
21
|
+
return pending;
|
|
22
|
+
const promise = createConnection(serverId);
|
|
23
|
+
pendingConnections.set(serverId, promise);
|
|
24
|
+
try {
|
|
25
|
+
return await promise;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
pendingConnections.delete(serverId);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function createConnection(serverId) {
|
|
32
|
+
// Cleanup old
|
|
33
|
+
const old = pool.get(serverId);
|
|
34
|
+
if (old) {
|
|
35
|
+
old.sftp = null;
|
|
36
|
+
old.sftpPending = null;
|
|
37
|
+
try {
|
|
38
|
+
old.client.end();
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
pool.delete(serverId);
|
|
42
|
+
}
|
|
43
|
+
const serverConfig = getServer(serverId);
|
|
44
|
+
const client = new Client();
|
|
45
|
+
const connectConfig = {
|
|
46
|
+
host: serverConfig.host,
|
|
47
|
+
port: serverConfig.port,
|
|
48
|
+
username: serverConfig.username,
|
|
49
|
+
readyTimeout: CONNECTION_TIMEOUT,
|
|
50
|
+
keepaliveInterval: KEEPALIVE_INTERVAL,
|
|
51
|
+
keepaliveCountMax: 5,
|
|
52
|
+
algorithms: {
|
|
53
|
+
// Prefer fast ciphers
|
|
54
|
+
cipher: ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr'],
|
|
55
|
+
// Prefer fast key exchange
|
|
56
|
+
kex: ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'diffie-hellman-group14-sha256'],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
if (serverConfig.privateKeyPath) {
|
|
60
|
+
try {
|
|
61
|
+
connectConfig.privateKey = readFileSync(serverConfig.privateKeyPath);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new Error(`Cannot read private key "${serverConfig.privateKeyPath}": ${err.message}`);
|
|
65
|
+
}
|
|
66
|
+
if (serverConfig.passphrase)
|
|
67
|
+
connectConfig.passphrase = serverConfig.passphrase;
|
|
68
|
+
}
|
|
69
|
+
else if (serverConfig.password) {
|
|
70
|
+
connectConfig.password = serverConfig.password;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
connectConfig.agent = process.env.SSH_AUTH_SOCK;
|
|
74
|
+
}
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const timeout = setTimeout(() => {
|
|
77
|
+
client.end();
|
|
78
|
+
reject(new Error(`Connection to "${serverId}" timed out (${CONNECTION_TIMEOUT}ms)`));
|
|
79
|
+
}, CONNECTION_TIMEOUT + 2000);
|
|
80
|
+
client.on('ready', () => {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
pool.set(serverId, { client, config: serverConfig, lastUsed: Date.now(), connected: true, sftp: null, sftpPending: null });
|
|
83
|
+
resolve(client);
|
|
84
|
+
});
|
|
85
|
+
client.on('error', (err) => {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
const p = pool.get(serverId);
|
|
88
|
+
if (p) {
|
|
89
|
+
p.connected = false;
|
|
90
|
+
p.sftp = null;
|
|
91
|
+
p.sftpPending = null;
|
|
92
|
+
}
|
|
93
|
+
reject(new Error(`SSH error "${serverId}": ${err.message}`));
|
|
94
|
+
});
|
|
95
|
+
client.on('close', () => {
|
|
96
|
+
const p = pool.get(serverId);
|
|
97
|
+
if (p) {
|
|
98
|
+
p.connected = false;
|
|
99
|
+
p.sftp = null;
|
|
100
|
+
p.sftpPending = null;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
client.on('end', () => {
|
|
104
|
+
const p = pool.get(serverId);
|
|
105
|
+
if (p) {
|
|
106
|
+
p.connected = false;
|
|
107
|
+
p.sftp = null;
|
|
108
|
+
p.sftpPending = null;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
client.connect(connectConfig);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get cached SFTP session or create one. Deduplicates concurrent requests.
|
|
116
|
+
*/
|
|
117
|
+
export async function getSftp(serverId) {
|
|
118
|
+
await getConnection(serverId); // Ensure connected
|
|
119
|
+
const pooled = pool.get(serverId);
|
|
120
|
+
// Return cached
|
|
121
|
+
if (pooled.sftp) {
|
|
122
|
+
pooled.lastUsed = Date.now();
|
|
123
|
+
return pooled.sftp;
|
|
124
|
+
}
|
|
125
|
+
// Dedup concurrent SFTP requests
|
|
126
|
+
if (pooled.sftpPending)
|
|
127
|
+
return pooled.sftpPending;
|
|
128
|
+
const promise = new Promise((resolve, reject) => {
|
|
129
|
+
pooled.client.sftp((err, sftp) => {
|
|
130
|
+
if (err) {
|
|
131
|
+
pooled.sftpPending = null;
|
|
132
|
+
reject(new Error(`SFTP failed "${serverId}": ${err.message}`));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
pooled.sftp = sftp;
|
|
136
|
+
pooled.sftpPending = null;
|
|
137
|
+
pooled.lastUsed = Date.now();
|
|
138
|
+
// Clear cache if SFTP session closes
|
|
139
|
+
sftp.on('close', () => { pooled.sftp = null; });
|
|
140
|
+
sftp.on('end', () => { pooled.sftp = null; });
|
|
141
|
+
resolve(sftp);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
pooled.sftpPending = promise;
|
|
146
|
+
return promise;
|
|
147
|
+
}
|
|
148
|
+
export function closeAll() {
|
|
149
|
+
for (const [, p] of pool) {
|
|
150
|
+
try {
|
|
151
|
+
p.client.end();
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
}
|
|
155
|
+
pool.clear();
|
|
156
|
+
}
|
|
157
|
+
export function disconnect(serverId) {
|
|
158
|
+
const p = pool.get(serverId);
|
|
159
|
+
if (p) {
|
|
160
|
+
try {
|
|
161
|
+
p.client.end();
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
pool.delete(serverId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Idle cleanup
|
|
168
|
+
setInterval(() => {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
for (const [id, p] of pool) {
|
|
171
|
+
if (now - p.lastUsed > IDLE_TIMEOUT) {
|
|
172
|
+
try {
|
|
173
|
+
p.client.end();
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
pool.delete(id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, 60000).unref();
|
|
180
|
+
process.on('exit', closeAll);
|
|
181
|
+
process.on('SIGINT', () => { closeAll(); process.exit(0); });
|
|
182
|
+
process.on('SIGTERM', () => { closeAll(); process.exit(0); });
|
|
183
|
+
//# sourceMappingURL=ssh-manager.js.map
|