ssh-mcp 1.0.7 → 1.0.9

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 +3 -1
  2. package/build/index.js +37 -13
  3. package/package.json +15 -7
package/README.md CHANGED
@@ -6,9 +6,11 @@
6
6
  [![License](https://img.shields.io/github/license/tufantunc/ssh-mcp)](./LICENSE)
7
7
  [![GitHub Stars](https://img.shields.io/github/stars/tufantunc/ssh-mcp?style=social)](https://github.com/tufantunc/ssh-mcp/stargazers)
8
8
  [![GitHub Forks](https://img.shields.io/github/forks/tufantunc/ssh-mcp?style=social)](https://github.com/tufantunc/ssh-mcp/forks)
9
- [![Build Status](https://github.com/tufantunc/ssh-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/tufantunc/ssh-mcp/actions)
9
+ [![Build Status](https://github.com/tufantunc/ssh-mcp/actions/workflows/publish.yml/badge.svg)](https://github.com/tufantunc/ssh-mcp/actions)
10
10
  [![GitHub issues](https://img.shields.io/github/issues/tufantunc/ssh-mcp)](https://github.com/tufantunc/ssh-mcp/issues)
11
11
 
12
+ [![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/tufantunc/ssh-mcp)](https://archestra.ai/mcp-catalog/tufantunc__ssh-mcp)
13
+
12
14
  **SSH MCP Server** is a local Model Context Protocol (MCP) server that exposes SSH control for Linux and Windows systems, enabling LLMs and other MCP clients to execute shell commands securely via SSH.
13
15
 
14
16
  ## Contents
package/build/index.js CHANGED
@@ -16,7 +16,8 @@ function parseArgv() {
16
16
  }
17
17
  return config;
18
18
  }
19
- const argvConfig = parseArgv();
19
+ const isCliEnabled = process.env.SSH_MCP_DISABLE_MAIN !== '1';
20
+ const argvConfig = isCliEnabled ? parseArgv() : {};
20
21
  const HOST = argvConfig.host;
21
22
  const PORT = argvConfig.port ? parseInt(argvConfig.port) : 22;
22
23
  const USER = argvConfig.user;
@@ -35,10 +36,32 @@ function validateConfig(config) {
35
36
  throw new Error('Configuration error:\n' + errors.join('\n'));
36
37
  }
37
38
  }
38
- validateConfig(argvConfig);
39
+ if (isCliEnabled) {
40
+ validateConfig(argvConfig);
41
+ }
42
+ // Command sanitization and validation
43
+ export function sanitizeCommand(command) {
44
+ if (typeof command !== 'string') {
45
+ throw new McpError(ErrorCode.InvalidParams, 'Command must be a string');
46
+ }
47
+ const trimmedCommand = command.trim();
48
+ if (!trimmedCommand) {
49
+ throw new McpError(ErrorCode.InvalidParams, 'Command cannot be empty');
50
+ }
51
+ // Length check
52
+ if (trimmedCommand.length > 1000) {
53
+ throw new McpError(ErrorCode.InvalidParams, 'Command is too long (max 1000 characters)');
54
+ }
55
+ return trimmedCommand;
56
+ }
57
+ // Escape command for use in shell contexts (like pkill)
58
+ export function escapeCommandForShell(command) {
59
+ // Replace single quotes with escaped single quotes
60
+ return command.replace(/'/g, "'\"'\"'");
61
+ }
39
62
  const server = new McpServer({
40
63
  name: 'SSH MCP Server',
41
- version: '1.0.7',
64
+ version: '1.0.9',
42
65
  capabilities: {
43
66
  resources: {},
44
67
  tools: {},
@@ -48,9 +71,7 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
48
71
  command: z.string().describe("Shell command to execute on the remote SSH server"),
49
72
  }, async ({ command }) => {
50
73
  // Sanitize command input
51
- if (typeof command !== 'string' || !command.trim()) {
52
- throw new McpError(ErrorCode.InternalError, 'Command must be a non-empty string.');
53
- }
74
+ const sanitizedCommand = sanitizeCommand(command);
54
75
  const sshConfig = {
55
76
  host: HOST,
56
77
  port: PORT,
@@ -64,7 +85,7 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
64
85
  const fs = await import('fs/promises');
65
86
  sshConfig.privateKey = await fs.readFile(KEY, 'utf8');
66
87
  }
67
- const result = await execSshCommand(sshConfig, command);
88
+ const result = await execSshCommand(sshConfig, sanitizedCommand);
68
89
  return result;
69
90
  }
70
91
  catch (err) {
@@ -74,7 +95,7 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
74
95
  throw new McpError(ErrorCode.InternalError, `Unexpected error: ${err?.message || err}`);
75
96
  }
76
97
  });
77
- async function execSshCommand(sshConfig, command) {
98
+ export async function execSshCommand(sshConfig, command) {
78
99
  return new Promise((resolve, reject) => {
79
100
  const conn = new SSHClient();
80
101
  let timeoutId;
@@ -88,7 +109,7 @@ async function execSshCommand(sshConfig, command) {
88
109
  // If abort command itself times out, force close connection
89
110
  conn.end();
90
111
  }, 5000); // 5 second timeout for abort command
91
- conn.exec('timeout 3s pkill -f "' + command + '" 2>/dev/null || true', (err, abortStream) => {
112
+ conn.exec('timeout 3s pkill -f \'' + escapeCommandForShell(command) + '\' 2>/dev/null || true', (err, abortStream) => {
92
113
  if (abortStream) {
93
114
  abortStream.on('close', () => {
94
115
  clearTimeout(abortTimeout);
@@ -157,7 +178,10 @@ async function main() {
157
178
  await server.connect(transport);
158
179
  console.error("SSH MCP Server running on stdio");
159
180
  }
160
- main().catch((error) => {
161
- console.error("Fatal error in main():", error);
162
- process.exit(1);
163
- });
181
+ if (process.env.SSH_MCP_DISABLE_MAIN !== '1') {
182
+ main().catch((error) => {
183
+ console.error("Fatal error in main():", error);
184
+ process.exit(1);
185
+ });
186
+ }
187
+ export { parseArgv, validateConfig };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ssh-mcp",
3
3
  "license": "MIT",
4
- "version": "1.0.7",
4
+ "version": "1.0.9",
5
5
  "description": "MCP server exposing SSH control for Linux and Windows systems via Model Context Protocol.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -10,21 +10,29 @@
10
10
  "scripts": {
11
11
  "prepare": "npm run build",
12
12
  "build": "tsc && shx chmod +x build/*.js",
13
- "inspect": "npx @modelcontextprotocol/inspector node build/index.js"
13
+ "inspect": "npx @modelcontextprotocol/inspector node build/index.js",
14
+ "test": "cross-env SSH_MCP_DISABLE_MAIN=1 vitest --run",
15
+ "test:watch": "cross-env SSH_MCP_DISABLE_MAIN=1 vitest",
16
+ "coverage": "cross-env SSH_MCP_DISABLE_MAIN=1 vitest --run --coverage"
14
17
  },
15
18
  "files": [
16
19
  "build"
17
20
  ],
18
21
  "dependencies": {
19
- "@modelcontextprotocol/sdk": "^1.10.2",
20
- "ssh2": "^1.11.0",
21
- "zod": "^3.24.3"
22
+ "@modelcontextprotocol/sdk": "^1.17.5",
23
+ "ssh2": "^1.17.0",
24
+ "zod": "3.23.8"
22
25
  },
23
26
  "devDependencies": {
24
- "@types/node": "^20.17.31",
27
+ "@types/node": "^24.5.2",
25
28
  "@types/ssh2": "^1.15.5",
29
+ "cross-env": "^7.0.3",
26
30
  "shx": "^0.4.0",
27
- "typescript": "^5.4.5"
31
+ "testcontainers": "^11.7.0",
32
+ "ts-node": "^10.9.2",
33
+ "tsx": "^4.20.6",
34
+ "typescript": "^5.9.2",
35
+ "vitest": "^3.2.4"
28
36
  },
29
37
  "homepage": "https://github.com/tufantunc/ssh-mcp#readme",
30
38
  "repository": {