ssh-mcp 1.0.8 → 1.1.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 +7 -1
- package/build/index.js +39 -12
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
- Timeout is configured via command line argument `--timeout` (in milliseconds)
|
|
49
49
|
- Default timeout: 60000ms (1 minute)
|
|
50
50
|
- When a command times out, the server automatically attempts to abort the running process before closing the connection
|
|
51
|
+
- **Max Command Length Configuration:**
|
|
52
|
+
- Max command characters are configured via `--maxChars`
|
|
53
|
+
- Default: `1000`
|
|
54
|
+
- No-limit mode: set `--maxChars=none` or any `<= 0` value (e.g. `--maxChars=0`)
|
|
51
55
|
|
|
52
56
|
## Installation
|
|
53
57
|
|
|
@@ -74,6 +78,7 @@ You can configure Claude Desktop to use this MCP Server.
|
|
|
74
78
|
- `password`: SSH password (or use `key` for key-based auth)
|
|
75
79
|
- `key`: Path to private SSH key
|
|
76
80
|
- `timeout`: Command execution timeout in milliseconds (default: 60000ms = 1 minute)
|
|
81
|
+
- `maxChars`: Maximum allowed characters for the `command` input (default: 1000). Use `none` or `0` to disable the limit.
|
|
77
82
|
|
|
78
83
|
|
|
79
84
|
```commandline
|
|
@@ -90,7 +95,8 @@ You can configure Claude Desktop to use this MCP Server.
|
|
|
90
95
|
"--user=root",
|
|
91
96
|
"--password=pass",
|
|
92
97
|
"--key=path/to/key",
|
|
93
|
-
"--timeout=30000"
|
|
98
|
+
"--timeout=30000",
|
|
99
|
+
"--maxChars=none"
|
|
94
100
|
]
|
|
95
101
|
}
|
|
96
102
|
}
|
package/build/index.js
CHANGED
|
@@ -16,13 +16,35 @@ function parseArgv() {
|
|
|
16
16
|
}
|
|
17
17
|
return config;
|
|
18
18
|
}
|
|
19
|
-
const
|
|
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;
|
|
23
24
|
const PASSWORD = argvConfig.password;
|
|
24
25
|
const KEY = argvConfig.key;
|
|
25
26
|
const DEFAULT_TIMEOUT = argvConfig.timeout ? parseInt(argvConfig.timeout) : 60000; // 60 seconds default timeout
|
|
27
|
+
// Max characters configuration:
|
|
28
|
+
// - Default: 1000 characters
|
|
29
|
+
// - When set via --maxChars:
|
|
30
|
+
// * a positive integer enforces that limit
|
|
31
|
+
// * 0 or a negative value disables the limit (no max)
|
|
32
|
+
// * the string "none" (case-insensitive) disables the limit (no max)
|
|
33
|
+
const MAX_CHARS_RAW = argvConfig.maxChars;
|
|
34
|
+
const MAX_CHARS = (() => {
|
|
35
|
+
if (typeof MAX_CHARS_RAW === 'string') {
|
|
36
|
+
const lowered = MAX_CHARS_RAW.toLowerCase();
|
|
37
|
+
if (lowered === 'none')
|
|
38
|
+
return Infinity;
|
|
39
|
+
const parsed = parseInt(MAX_CHARS_RAW);
|
|
40
|
+
if (isNaN(parsed))
|
|
41
|
+
return 1000;
|
|
42
|
+
if (parsed <= 0)
|
|
43
|
+
return Infinity;
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
return 1000;
|
|
47
|
+
})();
|
|
26
48
|
function validateConfig(config) {
|
|
27
49
|
const errors = [];
|
|
28
50
|
if (!config.host)
|
|
@@ -35,9 +57,11 @@ function validateConfig(config) {
|
|
|
35
57
|
throw new Error('Configuration error:\n' + errors.join('\n'));
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
|
-
|
|
60
|
+
if (isCliEnabled) {
|
|
61
|
+
validateConfig(argvConfig);
|
|
62
|
+
}
|
|
39
63
|
// Command sanitization and validation
|
|
40
|
-
function sanitizeCommand(command) {
|
|
64
|
+
export function sanitizeCommand(command) {
|
|
41
65
|
if (typeof command !== 'string') {
|
|
42
66
|
throw new McpError(ErrorCode.InvalidParams, 'Command must be a string');
|
|
43
67
|
}
|
|
@@ -46,19 +70,19 @@ function sanitizeCommand(command) {
|
|
|
46
70
|
throw new McpError(ErrorCode.InvalidParams, 'Command cannot be empty');
|
|
47
71
|
}
|
|
48
72
|
// Length check
|
|
49
|
-
if (trimmedCommand.length >
|
|
50
|
-
throw new McpError(ErrorCode.InvalidParams,
|
|
73
|
+
if (Number.isFinite(MAX_CHARS) && trimmedCommand.length > MAX_CHARS) {
|
|
74
|
+
throw new McpError(ErrorCode.InvalidParams, `Command is too long (max ${MAX_CHARS} characters)`);
|
|
51
75
|
}
|
|
52
76
|
return trimmedCommand;
|
|
53
77
|
}
|
|
54
78
|
// Escape command for use in shell contexts (like pkill)
|
|
55
|
-
function escapeCommandForShell(command) {
|
|
79
|
+
export function escapeCommandForShell(command) {
|
|
56
80
|
// Replace single quotes with escaped single quotes
|
|
57
81
|
return command.replace(/'/g, "'\"'\"'");
|
|
58
82
|
}
|
|
59
83
|
const server = new McpServer({
|
|
60
84
|
name: 'SSH MCP Server',
|
|
61
|
-
version: '1.0.
|
|
85
|
+
version: '1.0.9',
|
|
62
86
|
capabilities: {
|
|
63
87
|
resources: {},
|
|
64
88
|
tools: {},
|
|
@@ -92,7 +116,7 @@ server.tool("exec", "Execute a shell command on the remote SSH server and return
|
|
|
92
116
|
throw new McpError(ErrorCode.InternalError, `Unexpected error: ${err?.message || err}`);
|
|
93
117
|
}
|
|
94
118
|
});
|
|
95
|
-
async function execSshCommand(sshConfig, command) {
|
|
119
|
+
export async function execSshCommand(sshConfig, command) {
|
|
96
120
|
return new Promise((resolve, reject) => {
|
|
97
121
|
const conn = new SSHClient();
|
|
98
122
|
let timeoutId;
|
|
@@ -175,7 +199,10 @@ async function main() {
|
|
|
175
199
|
await server.connect(transport);
|
|
176
200
|
console.error("SSH MCP Server running on stdio");
|
|
177
201
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
if (process.env.SSH_MCP_DISABLE_MAIN !== '1') {
|
|
203
|
+
main().catch((error) => {
|
|
204
|
+
console.error("Fatal error in main():", error);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
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
|
|
4
|
+
"version": "1.1.0",
|
|
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,7 +10,10 @@
|
|
|
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"
|
|
@@ -18,13 +21,18 @@
|
|
|
18
21
|
"dependencies": {
|
|
19
22
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
20
23
|
"ssh2": "^1.17.0",
|
|
21
|
-
"zod": "
|
|
24
|
+
"zod": "3.23.8"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
|
-
"@types/node": "^24.
|
|
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
|
-
"
|
|
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": {
|