@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/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
@@ -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":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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