@aaarc/handfree-ssh-mcp 1.0.0 → 1.0.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.
- package/README.md +166 -133
- package/build/config/config-loader.js +60 -0
- package/build/config/server.js +13 -13
- package/build/config/ssh-config-loader.js +45 -2
- package/build/core/mcp-server.js +0 -5
- package/build/services/ssh-connection-manager.js +274 -164
- package/build/tools/execute-command.js +1 -1
- package/build/tools/help.js +4 -4
- package/build/tools/list-servers.js +6 -4
- package/build/tools/show-whitelist.js +67 -42
- package/package.json +20 -11
- package/build/tests/command-validation.test.js +0 -876
- package/build/tests/config.test.js +0 -617
- package/build/tests/integration.test.js +0 -348
- package/build/tests/output-collector.test.js +0 -70
- package/build/tests/output-log-writer.test.js +0 -205
- package/build/tests/recent-fixes.test.js +0 -229
- package/build/tests/tool-error.test.js +0 -26
- package/build/tests/tools.test.js +0 -486
|
@@ -1,7 +1,71 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { SSHConnectionManager } from "../services/ssh-connection-manager.js";
|
|
2
|
+
import { BUILT_IN_COMMAND_BLACKLIST, BUILT_IN_DESTRUCTIVE_GUARDS, SSHConnectionManager, } from "../services/ssh-connection-manager.js";
|
|
3
3
|
import { Logger } from "../utils/logger.js";
|
|
4
4
|
import { ToolError, formatToolErrorResponse, toToolError } from "../utils/tool-error.js";
|
|
5
|
+
export function formatCommandPolicy(config) {
|
|
6
|
+
const whitelist = config.commandWhitelist || [];
|
|
7
|
+
const blacklist = config.commandBlacklist || [];
|
|
8
|
+
const commandMode = config.commandMode
|
|
9
|
+
?? (whitelist.length > 0 ? "whitelist" : "blacklist");
|
|
10
|
+
let output = `## Command Policy\n\n`;
|
|
11
|
+
output += `Mode: \`${commandMode}\``;
|
|
12
|
+
if (!config.commandMode && whitelist.length > 0) {
|
|
13
|
+
output += ` _(legacy whitelist config)_`;
|
|
14
|
+
}
|
|
15
|
+
output += `\n\n`;
|
|
16
|
+
output += `Built-in destructive command guards:\n\n`;
|
|
17
|
+
for (const { regex, reason } of BUILT_IN_DESTRUCTIVE_GUARDS) {
|
|
18
|
+
output += `- \`${regex.source}\` -> ${reason}\n`;
|
|
19
|
+
}
|
|
20
|
+
output += `\n`;
|
|
21
|
+
output += `Built-in dangerous-command blacklist:\n\n`;
|
|
22
|
+
for (const { regex, reason } of BUILT_IN_COMMAND_BLACKLIST) {
|
|
23
|
+
output += `- \`${regex.source}\` -> ${reason}\n`;
|
|
24
|
+
}
|
|
25
|
+
output += `\n`;
|
|
26
|
+
output += `## Allowed Commands (Whitelist)\n\n`;
|
|
27
|
+
if (commandMode !== "whitelist") {
|
|
28
|
+
output += `Whitelist is inactive in blacklist mode.\n\n`;
|
|
29
|
+
}
|
|
30
|
+
else if (whitelist.length === 0) {
|
|
31
|
+
output += `WARNING: Whitelist mode is active but no whitelist patterns are configured, so all commands are blocked after blacklist checks.\n\n`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
output += `${whitelist.length} patterns:\n\n`;
|
|
35
|
+
for (const pattern of whitelist) {
|
|
36
|
+
const readable = patternToReadable(pattern);
|
|
37
|
+
output += `- \`${pattern}\``;
|
|
38
|
+
if (readable !== pattern) {
|
|
39
|
+
output += ` -> ${readable}`;
|
|
40
|
+
}
|
|
41
|
+
output += `\n`;
|
|
42
|
+
}
|
|
43
|
+
output += `\n`;
|
|
44
|
+
}
|
|
45
|
+
if (blacklist.length > 0) {
|
|
46
|
+
output += `## Blocked Commands (Blacklist)\n\n`;
|
|
47
|
+
output += `${blacklist.length} patterns:\n\n`;
|
|
48
|
+
for (const pattern of blacklist) {
|
|
49
|
+
const readable = patternToReadable(pattern);
|
|
50
|
+
output += `- \`${pattern}\``;
|
|
51
|
+
if (readable !== pattern) {
|
|
52
|
+
output += ` -> ${readable}`;
|
|
53
|
+
}
|
|
54
|
+
output += `\n`;
|
|
55
|
+
}
|
|
56
|
+
output += `\n`;
|
|
57
|
+
}
|
|
58
|
+
if (commandMode === "whitelist" && whitelist.length > 0) {
|
|
59
|
+
output += `## Example Commands\n\n`;
|
|
60
|
+
output += `Based on the active whitelist, here are some commands you can likely use:\n\n`;
|
|
61
|
+
const examples = generateExamples(whitelist);
|
|
62
|
+
for (const ex of examples.slice(0, 10)) {
|
|
63
|
+
output += `- \`${ex}\`\n`;
|
|
64
|
+
}
|
|
65
|
+
output += `\n`;
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
5
69
|
/**
|
|
6
70
|
* Register show-whitelist tool
|
|
7
71
|
*
|
|
@@ -9,7 +73,7 @@ import { ToolError, formatToolErrorResponse, toToolError } from "../utils/tool-e
|
|
|
9
73
|
*/
|
|
10
74
|
export function registerShowWhitelistTool(server) {
|
|
11
75
|
const sshManager = SSHConnectionManager.getInstance();
|
|
12
|
-
server.tool("show-whitelist", "Show the configured
|
|
76
|
+
server.tool("show-whitelist", "Show the configured command policy, blacklist/whitelist patterns, and SFTP path policy (allowedRemoteDirectories / allowedLocalDirectories) for a server. Use this before execute-command when you need to understand which commands are allowed, why a command may be rejected, or what command patterns are safe to try next. Also use it before upload / download / transfer to see which paths SFTP is permitted to touch.", {
|
|
13
77
|
connectionName: z
|
|
14
78
|
.string()
|
|
15
79
|
.optional()
|
|
@@ -28,41 +92,9 @@ export function registerShowWhitelistTool(server) {
|
|
|
28
92
|
isError: true,
|
|
29
93
|
};
|
|
30
94
|
}
|
|
31
|
-
const whitelist = config.commandWhitelist || [];
|
|
32
|
-
const blacklist = config.commandBlacklist || [];
|
|
33
95
|
let output = `# Command Permissions for: ${config.name}\n\n`;
|
|
34
96
|
output += `Host: ${config.username}@${config.host}:${config.port}\n\n`;
|
|
35
|
-
|
|
36
|
-
output += `## ✅ Allowed Commands (Whitelist)\n\n`;
|
|
37
|
-
if (whitelist.length === 0) {
|
|
38
|
-
output += `⚠️ No whitelist configured - using default safe commands.\n\n`;
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
output += `${whitelist.length} patterns:\n\n`;
|
|
42
|
-
for (const pattern of whitelist) {
|
|
43
|
-
const readable = patternToReadable(pattern);
|
|
44
|
-
output += `- \`${pattern}\``;
|
|
45
|
-
if (readable !== pattern) {
|
|
46
|
-
output += ` → ${readable}`;
|
|
47
|
-
}
|
|
48
|
-
output += `\n`;
|
|
49
|
-
}
|
|
50
|
-
output += `\n`;
|
|
51
|
-
}
|
|
52
|
-
// Blacklist
|
|
53
|
-
if (blacklist.length > 0) {
|
|
54
|
-
output += `## ❌ Blocked Commands (Blacklist)\n\n`;
|
|
55
|
-
output += `${blacklist.length} patterns:\n\n`;
|
|
56
|
-
for (const pattern of blacklist) {
|
|
57
|
-
const readable = patternToReadable(pattern);
|
|
58
|
-
output += `- \`${pattern}\``;
|
|
59
|
-
if (readable !== pattern) {
|
|
60
|
-
output += ` → ${readable}`;
|
|
61
|
-
}
|
|
62
|
-
output += `\n`;
|
|
63
|
-
}
|
|
64
|
-
output += `\n`;
|
|
65
|
-
}
|
|
97
|
+
output += formatCommandPolicy(config);
|
|
66
98
|
// SFTP path policy (upload / download / transfer tools only)
|
|
67
99
|
const allowedRemoteDirs = config.allowedRemoteDirectories ?? [];
|
|
68
100
|
const allowedLocalDirs = config.allowedLocalDirectories ?? [];
|
|
@@ -112,13 +144,6 @@ export function registerShowWhitelistTool(server) {
|
|
|
112
144
|
output += `- This server's logs land under: \`${serverLogDir}/\`\n`;
|
|
113
145
|
output += `- File name: \`<timestamp>-<pid>-<rand>.log\` with \`=== META / STDOUT / STDERR / END ===\` markers.\n`;
|
|
114
146
|
output += `- When output is truncated, the response includes an \`[OUTPUT TRUNCATED]\` header with the on-disk log path.\n\n`;
|
|
115
|
-
// Quick examples
|
|
116
|
-
output += `## 💡 Example Commands\n\n`;
|
|
117
|
-
output += `Based on the whitelist, here are some commands you can likely use:\n\n`;
|
|
118
|
-
const examples = generateExamples(whitelist);
|
|
119
|
-
for (const ex of examples.slice(0, 10)) {
|
|
120
|
-
output += `- \`${ex}\`\n`;
|
|
121
|
-
}
|
|
122
147
|
return {
|
|
123
148
|
content: [{ type: "text", text: output }],
|
|
124
149
|
};
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@aaarc/handfree-ssh-mcp",
|
|
3
|
-
"version": "1.0.
|
|
2
|
+
"name": "@aaarc/handfree-ssh-mcp",
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Handfree SSH MCP Server - A hands-free SSH automation tool via MCP protocol",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"handfree-ssh-mcp": "build/index.js"
|
|
9
9
|
},
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/Ironboxplus/handfree-ssh-mcp.git"
|
|
13
|
-
},
|
|
14
|
-
"bugs": {
|
|
15
|
-
"url": "https://github.com/Ironboxplus/handfree-ssh-mcp/issues"
|
|
16
|
-
},
|
|
17
|
-
"homepage": "https://github.com/Ironboxplus/handfree-ssh-mcp#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/Ironboxplus/handfree-ssh-mcp.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Ironboxplus/handfree-ssh-mcp/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/Ironboxplus/handfree-ssh-mcp#readme",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
18
21
|
"scripts": {
|
|
19
22
|
"test": "npm run build && node --test build/tests/config.test.js build/tests/command-validation.test.js build/tests/tool-error.test.js build/tests/tools.test.js build/tests/output-collector.test.js build/tests/output-log-writer.test.js build/tests/recent-fixes.test.js",
|
|
20
23
|
"test:config": "npm run build && node --test build/tests/config.test.js",
|
|
@@ -47,6 +50,12 @@
|
|
|
47
50
|
"typescript": "^5.8.2"
|
|
48
51
|
},
|
|
49
52
|
"files": [
|
|
50
|
-
"build/**/*"
|
|
53
|
+
"build/config/**/*",
|
|
54
|
+
"build/core/**/*",
|
|
55
|
+
"build/models/**/*",
|
|
56
|
+
"build/services/**/*",
|
|
57
|
+
"build/tools/**/*",
|
|
58
|
+
"build/utils/**/*",
|
|
59
|
+
"build/index.js"
|
|
51
60
|
]
|
|
52
61
|
}
|