ssh-mcp 1.1.0 → 1.2.2

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 (3) hide show
  1. package/README.md +57 -1
  2. package/build/index.js +180 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -67,7 +67,7 @@
67
67
 
68
68
  ## Client Setup
69
69
 
70
- You can configure Claude Desktop to use this MCP Server.
70
+ You can configure your IDE or LLM like Cursor, Windsurf, Claude Desktop to use this MCP Server.
71
71
 
72
72
  **Required Parameters:**
73
73
  - `host`: Hostname or IP of the Linux or Windows server
@@ -103,6 +103,62 @@ You can configure Claude Desktop to use this MCP Server.
103
103
  }
104
104
  ```
105
105
 
106
+ ### Claude Code
107
+
108
+ You can add this MCP server to Claude Code using the `claude mcp add` command. This is the recommended method for Claude Code.
109
+
110
+ **Basic Installation:**
111
+
112
+ ```bash
113
+ claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=YOUR_HOST --user=YOUR_USER --password=YOUR_PASSWORD
114
+ ```
115
+
116
+ **Installation Examples:**
117
+
118
+ **With Password Authentication:**
119
+ ```bash
120
+ claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=192.168.1.100 --port=22 --user=admin --password=your_password
121
+ ```
122
+
123
+ **With SSH Key Authentication:**
124
+ ```bash
125
+ claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=example.com --user=root --key=/path/to/private/key
126
+ ```
127
+
128
+ **With Custom Timeout and No Character Limit:**
129
+ ```bash
130
+ claude mcp add --transport stdio ssh-mcp -- npx -y ssh-mcp -- --host=192.168.1.100 --user=admin --password=your_password --timeout=120000 --maxChars=none
131
+ ```
132
+
133
+ **Installation Scopes:**
134
+
135
+ You can specify the scope when adding the server:
136
+
137
+ - **Local scope** (default): For personal use in the current project
138
+ ```bash
139
+ claude mcp add --transport stdio ssh-mcp --scope local -- npx -y ssh-mcp -- --host=YOUR_HOST --user=YOUR_USER --password=YOUR_PASSWORD
140
+ ```
141
+
142
+ - **Project scope**: Share with your team via `.mcp.json` file
143
+ ```bash
144
+ claude mcp add --transport stdio ssh-mcp --scope project -- npx -y ssh-mcp -- --host=YOUR_HOST --user=YOUR_USER --password=YOUR_PASSWORD
145
+ ```
146
+
147
+ - **User scope**: Available across all your projects
148
+ ```bash
149
+ claude mcp add --transport stdio ssh-mcp --scope user -- npx -y ssh-mcp -- --host=YOUR_HOST --user=YOUR_USER --password=YOUR_PASSWORD
150
+ ```
151
+
152
+
153
+ **Verify Installation:**
154
+
155
+ After adding the server, restart Claude Code and ask Cascade to execute a command:
156
+ ```
157
+ "Can you run 'ls -la' on the remote server?"
158
+ ```
159
+
160
+ For more information about MCP in Claude Code, see the [official documentation](https://docs.claude.com/en/docs/claude-code/mcp).
161
+
106
162
  ## Testing
107
163
 
108
164
  You can use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) for visual debugging of this MCP Server.
package/build/index.js CHANGED
@@ -80,9 +80,86 @@ export function escapeCommandForShell(command) {
80
80
  // Replace single quotes with escaped single quotes
81
81
  return command.replace(/'/g, "'\"'\"'");
82
82
  }
83
+ // SSH Connection Manager to maintain persistent connection
84
+ export class SSHConnectionManager {
85
+ conn = null;
86
+ sshConfig = null;
87
+ isConnecting = false;
88
+ connectionPromise = null;
89
+ constructor(config) {
90
+ this.sshConfig = config;
91
+ }
92
+ async connect() {
93
+ if (this.conn && this.isConnected()) {
94
+ return; // Already connected
95
+ }
96
+ if (this.isConnecting && this.connectionPromise) {
97
+ return this.connectionPromise; // Wait for ongoing connection
98
+ }
99
+ this.isConnecting = true;
100
+ this.connectionPromise = new Promise((resolve, reject) => {
101
+ this.conn = new SSHClient();
102
+ const timeoutId = setTimeout(() => {
103
+ this.conn?.end();
104
+ this.conn = null;
105
+ this.isConnecting = false;
106
+ this.connectionPromise = null;
107
+ reject(new McpError(ErrorCode.InternalError, 'SSH connection timeout'));
108
+ }, 30000); // 30 seconds connection timeout
109
+ this.conn.on('ready', () => {
110
+ clearTimeout(timeoutId);
111
+ this.isConnecting = false;
112
+ console.error('SSH connection established');
113
+ resolve();
114
+ });
115
+ this.conn.on('error', (err) => {
116
+ clearTimeout(timeoutId);
117
+ this.conn = null;
118
+ this.isConnecting = false;
119
+ this.connectionPromise = null;
120
+ reject(new McpError(ErrorCode.InternalError, `SSH connection error: ${err.message}`));
121
+ });
122
+ this.conn.on('end', () => {
123
+ console.error('SSH connection ended');
124
+ this.conn = null;
125
+ this.isConnecting = false;
126
+ this.connectionPromise = null;
127
+ });
128
+ this.conn.on('close', () => {
129
+ console.error('SSH connection closed');
130
+ this.conn = null;
131
+ this.isConnecting = false;
132
+ this.connectionPromise = null;
133
+ });
134
+ this.conn.connect(this.sshConfig);
135
+ });
136
+ return this.connectionPromise;
137
+ }
138
+ isConnected() {
139
+ return this.conn !== null && this.conn._sock && !this.conn._sock.destroyed;
140
+ }
141
+ async ensureConnected() {
142
+ if (!this.isConnected()) {
143
+ await this.connect();
144
+ }
145
+ }
146
+ getConnection() {
147
+ if (!this.conn) {
148
+ throw new McpError(ErrorCode.InternalError, 'SSH connection not established');
149
+ }
150
+ return this.conn;
151
+ }
152
+ close() {
153
+ if (this.conn) {
154
+ this.conn.end();
155
+ this.conn = null;
156
+ }
157
+ }
158
+ }
159
+ let connectionManager = null;
83
160
  const server = new McpServer({
84
161
  name: 'SSH MCP Server',
85
- version: '1.0.9',
162
+ version: '1.2.2',
86
163
  capabilities: {
87
164
  resources: {},
88
165
  tools: {},
@@ -93,20 +170,26 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
93
170
  }, async ({ command }) => {
94
171
  // Sanitize command input
95
172
  const sanitizedCommand = sanitizeCommand(command);
96
- const sshConfig = {
97
- host: HOST,
98
- port: PORT,
99
- username: USER,
100
- };
101
173
  try {
102
- if (PASSWORD) {
103
- sshConfig.password = PASSWORD;
104
- }
105
- else if (KEY) {
106
- const fs = await import('fs/promises');
107
- sshConfig.privateKey = await fs.readFile(KEY, 'utf8');
174
+ // Initialize connection manager if not already done
175
+ if (!connectionManager) {
176
+ const sshConfig = {
177
+ host: HOST,
178
+ port: PORT,
179
+ username: USER,
180
+ };
181
+ if (PASSWORD) {
182
+ sshConfig.password = PASSWORD;
183
+ }
184
+ else if (KEY) {
185
+ const fs = await import('fs/promises');
186
+ sshConfig.privateKey = await fs.readFile(KEY, 'utf8');
187
+ }
188
+ connectionManager = new SSHConnectionManager(sshConfig);
108
189
  }
109
- const result = await execSshCommand(sshConfig, sanitizedCommand);
190
+ // Ensure connection is active (reconnect if needed)
191
+ await connectionManager.ensureConnected();
192
+ const result = await execSshCommandWithConnection(connectionManager, sanitizedCommand);
110
193
  return result;
111
194
  }
112
195
  catch (err) {
@@ -116,6 +199,71 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
116
199
  throw new McpError(ErrorCode.InternalError, `Unexpected error: ${err?.message || err}`);
117
200
  }
118
201
  });
202
+ // New function that uses persistent connection
203
+ export async function execSshCommandWithConnection(manager, command) {
204
+ return new Promise((resolve, reject) => {
205
+ let timeoutId;
206
+ let isResolved = false;
207
+ const conn = manager.getConnection();
208
+ // Set up timeout
209
+ timeoutId = setTimeout(() => {
210
+ if (!isResolved) {
211
+ isResolved = true;
212
+ // Try to abort the running command
213
+ const abortTimeout = setTimeout(() => {
214
+ // If abort command itself times out, we'll just reject
215
+ }, 5000);
216
+ conn.exec('timeout 3s pkill -f \'' + escapeCommandForShell(command) + '\' 2>/dev/null || true', (err, abortStream) => {
217
+ if (abortStream) {
218
+ abortStream.on('close', () => {
219
+ clearTimeout(abortTimeout);
220
+ });
221
+ }
222
+ else {
223
+ clearTimeout(abortTimeout);
224
+ }
225
+ });
226
+ reject(new McpError(ErrorCode.InternalError, `Command execution timed out after ${DEFAULT_TIMEOUT}ms`));
227
+ }
228
+ }, DEFAULT_TIMEOUT);
229
+ conn.exec(command, (err, stream) => {
230
+ if (err) {
231
+ if (!isResolved) {
232
+ isResolved = true;
233
+ clearTimeout(timeoutId);
234
+ reject(new McpError(ErrorCode.InternalError, `SSH exec error: ${err.message}`));
235
+ }
236
+ return;
237
+ }
238
+ let stdout = '';
239
+ let stderr = '';
240
+ stream.on('close', (code, signal) => {
241
+ if (!isResolved) {
242
+ isResolved = true;
243
+ clearTimeout(timeoutId);
244
+ if (stderr) {
245
+ reject(new McpError(ErrorCode.InternalError, `Error (code ${code}):\n${stderr}`));
246
+ }
247
+ else {
248
+ resolve({
249
+ content: [{
250
+ type: 'text',
251
+ text: stdout,
252
+ }],
253
+ });
254
+ }
255
+ }
256
+ });
257
+ stream.on('data', (data) => {
258
+ stdout += data.toString();
259
+ });
260
+ stream.stderr.on('data', (data) => {
261
+ stderr += data.toString();
262
+ });
263
+ });
264
+ });
265
+ }
266
+ // Keep the old function for backward compatibility (used in tests)
119
267
  export async function execSshCommand(sshConfig, command) {
120
268
  return new Promise((resolve, reject) => {
121
269
  const conn = new SSHClient();
@@ -198,10 +346,29 @@ async function main() {
198
346
  const transport = new StdioServerTransport();
199
347
  await server.connect(transport);
200
348
  console.error("SSH MCP Server running on stdio");
349
+ // Handle graceful shutdown
350
+ const cleanup = () => {
351
+ console.error("Shutting down SSH MCP Server...");
352
+ if (connectionManager) {
353
+ connectionManager.close();
354
+ connectionManager = null;
355
+ }
356
+ process.exit(0);
357
+ };
358
+ process.on('SIGINT', cleanup);
359
+ process.on('SIGTERM', cleanup);
360
+ process.on('exit', () => {
361
+ if (connectionManager) {
362
+ connectionManager.close();
363
+ }
364
+ });
201
365
  }
202
366
  if (process.env.SSH_MCP_DISABLE_MAIN !== '1') {
203
367
  main().catch((error) => {
204
368
  console.error("Fatal error in main():", error);
369
+ if (connectionManager) {
370
+ connectionManager.close();
371
+ }
205
372
  process.exit(1);
206
373
  });
207
374
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ssh-mcp",
3
3
  "license": "MIT",
4
- "version": "1.1.0",
4
+ "version": "1.2.2",
5
5
  "description": "MCP server exposing SSH control for Linux and Windows systems via Model Context Protocol.",
6
6
  "type": "module",
7
7
  "bin": {