@aaarc/handfree-ssh-mcp 1.0.3 → 1.0.5

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 CHANGED
@@ -216,7 +216,8 @@ servers:
216
216
 
217
217
  # SFTP path policy — applies ONLY to upload / download / transfer.
218
218
  # execute-command is NOT affected by these lists.
219
- # If `allowedRemoteDirectories` is unset or empty, SFTP is DISABLED for the server.
219
+ # Default is OPEN: with `allowedRemoteDirectories` unset/empty, any absolute
220
+ # POSIX remote path is allowed. Configure it to opt into an allowlist instead.
220
221
  allowedRemoteDirectories:
221
222
  - /home/user
222
223
  - /tmp
@@ -224,20 +225,23 @@ servers:
224
225
  # The MCP working directory is always permitted implicitly.
225
226
  allowedLocalDirectories:
226
227
  - /path/to/extra/local/dir
228
+ # Bypasses both the remote allowlist and the local directory check entirely.
229
+ # disableSftpPathPolicy: true
227
230
  ```
228
231
 
229
232
  ### Security note: command policy
230
233
 
231
- `execute-command` defaults to `commandMode: blacklist`. In that mode commands are allowed unless they match built-in destructive guards, the built-in dangerous-command blacklist, or a server's configured `blacklist:` patterns. Built-in blocked operations include hidden/chained destructive file operations, risky absolute-path output redirection, system power commands (`reboot`, `shutdown`, `halt`, `poweroff`), recursive force delete (`rm -rf`), destructive disk writes (`dd ... of=`), and filesystem formatting commands. Set `commandMode: whitelist` to require every command to match `whitelist:` after blacklist checks. For compatibility, a YAML server that contains `whitelist:` without `commandMode:` is treated as whitelist mode.
234
+ `execute-command` defaults to `commandMode: blacklist`. In that mode commands are allowed unless they match built-in destructive guards, the built-in dangerous-command blacklist, or a server's configured `blacklist:` patterns. Built-in blocked operations include hidden/chained destructive file operations, risky absolute-path output redirection, system power commands (`reboot`, `shutdown`, `halt`, `poweroff`), recursive force delete (`rm -rf`), destructive disk writes (`dd ... of=`), and filesystem formatting commands. Set `commandMode: whitelist` to require every command to match `whitelist:` after blacklist checks. For compatibility, a YAML server that contains `whitelist:` without `commandMode:` is treated as whitelist mode. Set `disableBuiltinGuards: true` and/or `disableBuiltinBlacklist: true` to turn off the built-in guards/blacklist for a server (your own `blacklist:`/`whitelist:` still apply).
232
235
 
233
236
  ### SFTP path policy
234
237
 
235
238
  | Field | Scope | Default behavior |
236
239
  |---|---|---|
237
- | `allowedRemoteDirectories` | `upload` / `download` / `transfer` only | **Unset = SFTP disabled.** Must list absolute POSIX directories. |
240
+ | `allowedRemoteDirectories` | `upload` / `download` / `transfer` only | **Unset/empty = open.** Any absolute POSIX path is allowed. Configuring this list opts into restricting SFTP to those directories. |
238
241
  | `allowedLocalDirectories` | `upload` / `download` only | Unset = only the MCP working directory is allowed. |
242
+ | `disableSftpPathPolicy` | all SFTP tools | When `true`, bypasses both checks above entirely — any remote path, any local path. |
239
243
 
240
- Path matching is exact-equal or `dir + separator` prefix. `..` segments and null bytes are rejected. Use `show-whitelist` to inspect a server's current SFTP policy.
244
+ Path matching (when an allowlist is configured) is exact-equal or `dir + separator` prefix. `..` segments and null bytes are always rejected regardless of policy. Use `show-whitelist` to inspect a server's current SFTP policy.
241
245
 
242
246
  ### Jump host (ProxyJump-style)
243
247
 
@@ -148,6 +148,12 @@ function buildYamlServerConfig(name, serverConfig, options = {}) {
148
148
  if (serverConfig.blacklist !== undefined) {
149
149
  merged.commandBlacklist = serverConfig.blacklist ?? undefined;
150
150
  }
151
+ if (serverConfig.disableBuiltinGuards !== undefined) {
152
+ merged.disableBuiltinGuards = serverConfig.disableBuiltinGuards === true;
153
+ }
154
+ if (serverConfig.disableBuiltinBlacklist !== undefined) {
155
+ merged.disableBuiltinBlacklist = serverConfig.disableBuiltinBlacklist === true;
156
+ }
151
157
  if (serverConfig.safeDirectory !== undefined) {
152
158
  merged.safeDirectory = serverConfig.safeDirectory ?? undefined;
153
159
  }
@@ -157,6 +163,9 @@ function buildYamlServerConfig(name, serverConfig, options = {}) {
157
163
  if (serverConfig.allowedLocalDirectories !== undefined) {
158
164
  merged.allowedLocalDirectories = normalizeAllowedLocalDirectories(name, serverConfig.allowedLocalDirectories);
159
165
  }
166
+ if (serverConfig.disableSftpPathPolicy !== undefined) {
167
+ merged.disableSftpPathPolicy = serverConfig.disableSftpPathPolicy === true;
168
+ }
160
169
  if (!allowPartial) {
161
170
  Logger.log(`Loaded server config: ${name} -> ${merged.username}@${merged.host}:${merged.port}`, "info");
162
171
  }
@@ -175,6 +184,9 @@ function mergeConfigMaps(sshConfigs, yamlConfigs) {
175
184
  commandMode: yamlConfig.commandMode ?? merged[name]?.commandMode,
176
185
  commandWhitelist: yamlConfig.commandWhitelist ?? merged[name]?.commandWhitelist,
177
186
  commandBlacklist: yamlConfig.commandBlacklist ?? merged[name]?.commandBlacklist,
187
+ disableBuiltinGuards: yamlConfig.disableBuiltinGuards ?? merged[name]?.disableBuiltinGuards,
188
+ disableBuiltinBlacklist: yamlConfig.disableBuiltinBlacklist ?? merged[name]?.disableBuiltinBlacklist,
189
+ disableSftpPathPolicy: yamlConfig.disableSftpPathPolicy ?? merged[name]?.disableSftpPathPolicy,
178
190
  safeDirectory: yamlConfig.safeDirectory ?? merged[name]?.safeDirectory,
179
191
  };
180
192
  }
@@ -1,6 +1,6 @@
1
1
  export const SERVER_CONFIG = {
2
2
  name: "ssh-mcp-server",
3
- version: "1.0.3",
3
+ version: "1.0.5",
4
4
  };
5
5
  export const SERVER_INSTRUCTIONS = `This server provides SSH access to servers loaded from OpenSSH config (~/.ssh/config by default) and optional YAML config/policy overlays.
6
6
 
@@ -29,23 +29,10 @@ export const BUILT_IN_COMMAND_BLACKLIST = [
29
29
  { regex: /\bRemove-Item\b(?=.*\s-Recurse(?:\s|$))(?=.*\s-Force(?:\s|$))/i, reason: "recursive force Remove-Item" },
30
30
  { regex: /^\s*(?:del|erase|rd)\b(?=.*(?:\/s\b|\s-Recurse(?:\s|$)))(?=.*(?:\/q\b|\s-Force(?:\s|$)))/i, reason: "recursive quiet Windows delete" },
31
31
  { regex: /^\s*(?:sudo\s+)?rmdir\s+(?:\/|\*|~|\$HOME|%USERPROFILE%|[A-Za-z]:\\)(?:\s|$)/i, reason: "dangerous rmdir target" },
32
- { regex: /^\s*(?:sudo\s+)?dd\b.*\bof=/i, reason: "dd with of= (can overwrite devices/files)" },
33
- { regex: /^\s*(?:sudo\s+)?mkfs(?:\.\S+)?\b/i, reason: "filesystem formatting command" },
34
- { regex: /^\s*(?:sudo\s+)?(?:diskpart|format)(?:\s|$)/i, reason: "disk formatting command" },
35
- { regex: /\b(?:Format-Volume|Clear-Disk|Remove-Partition)\b/i, reason: "Windows disk destructive command" },
36
32
  { regex: /^\s*(?:sudo\s+)?chmod\s+-R\s+777\b/i, reason: "recursive world-writable chmod" },
37
33
  { regex: /^\s*(?:sudo\s+)?chown\s+-R\s+\S+\s+\/(?:\s|$)/i, reason: "recursive chown on root" },
38
34
  ];
39
35
  export const BUILT_IN_DESTRUCTIVE_GUARDS = [
40
- { regex: /\brm\b/, reason: "rm in command chain" },
41
- { regex: /\brmdir\b/, reason: "rmdir in command chain" },
42
- { regex: /\bunlink\b/, reason: "unlink in command chain" },
43
- { regex: /\bshred\b/, reason: "shred in command chain" },
44
- { regex: /\btruncate\b/, reason: "truncate in command chain" },
45
- { regex: /-delete\b/, reason: "find -delete detected" },
46
- { regex: /-exec\s+rm\b/, reason: "find -exec rm detected" },
47
- { regex: /\bmv\b/, reason: "mv detected (can overwrite)" },
48
- { regex: /\bcp\b.*-f/, reason: "cp -f detected (force overwrite)" },
49
36
  { regex: /(?<![0-9])>\s*\/(?!dev\/null)/, reason: "output redirection to absolute path" },
50
37
  { regex: />\s*~/, reason: "output redirection to home path" },
51
38
  ];
@@ -824,7 +811,9 @@ export class SSHConnectionManager {
824
811
  || (config.username
825
812
  ? (config.username === 'root' ? '/root' : `/home/${config.username}`)
826
813
  : SSHConnectionManager.DEFAULT_SAFE_DIRECTORY);
827
- const destructiveReason = this.getDestructiveMatch(command);
814
+ const destructiveReason = config.disableBuiltinGuards
815
+ ? null
816
+ : this.getDestructiveMatch(command);
828
817
  if (destructiveReason) {
829
818
  // Command contains rm/rmdir - check if it's a simple rm command or hidden in a chain
830
819
  const trimmed = command.trim();
@@ -852,13 +841,15 @@ export class SSHConnectionManager {
852
841
  // ========================================
853
842
  // LAYER 2: Built-in blacklist for high-risk operations
854
843
  // ========================================
855
- for (const { regex, reason } of BUILT_IN_COMMAND_BLACKLIST) {
856
- if (regex.test(command)) {
857
- Logger.log(`Command blocked by built-in blacklist (${reason}): ${command}`, "info");
858
- return {
859
- isAllowed: false,
860
- reason: `Command blocked by built-in blacklist: ${reason}`,
861
- };
844
+ if (!config.disableBuiltinBlacklist) {
845
+ for (const { regex, reason } of BUILT_IN_COMMAND_BLACKLIST) {
846
+ if (regex.test(command)) {
847
+ Logger.log(`Command blocked by built-in blacklist (${reason}): ${command}`, "info");
848
+ return {
849
+ isAllowed: false,
850
+ reason: `Command blocked by built-in blacklist: ${reason}`,
851
+ };
852
+ }
862
853
  }
863
854
  }
864
855
  // ========================================
@@ -1213,14 +1204,16 @@ export class SSHConnectionManager {
1213
1204
  */
1214
1205
  validateLocalPath(localPath, name) {
1215
1206
  const resolvedPath = path.resolve(localPath);
1207
+ const config = name ? this.getServerConfig(name) : undefined;
1208
+ // disableSftpPathPolicy fully opens the local side too (any path allowed).
1209
+ if (config?.disableSftpPathPolicy) {
1210
+ return resolvedPath;
1211
+ }
1216
1212
  const allowedRoots = new Set([process.cwd()]);
1217
1213
  // Add per-server allowedLocalDirectories if a server is targeted
1218
- if (name) {
1219
- const config = this.getServerConfig(name);
1220
- if (config?.allowedLocalDirectories) {
1221
- for (const dir of config.allowedLocalDirectories) {
1222
- allowedRoots.add(dir);
1223
- }
1214
+ if (config?.allowedLocalDirectories) {
1215
+ for (const dir of config.allowedLocalDirectories) {
1216
+ allowedRoots.add(dir);
1224
1217
  }
1225
1218
  }
1226
1219
  const isAllowed = Array.from(allowedRoots).some((root) => resolvedPath === root || resolvedPath.startsWith(root + path.sep));
@@ -1258,9 +1251,11 @@ export class SSHConnectionManager {
1258
1251
  const normalized = path.posix.normalize(remotePath);
1259
1252
  const config = this.getConfig(name);
1260
1253
  const allowedRoots = config.allowedRemoteDirectories ?? [];
1261
- if (allowedRoots.length === 0) {
1262
- throw new ToolError("REMOTE_PATH_NOT_ALLOWED", `SFTP is disabled for server '${name}': no 'allowedRemoteDirectories' configured. ` +
1263
- `Add at least one absolute POSIX directory to 'allowedRemoteDirectories' in the YAML config to permit upload/download.`, false);
1254
+ // Default is OPEN: with no 'allowedRemoteDirectories' configured (or
1255
+ // disableSftpPathPolicy set), any absolute POSIX path is allowed. Configure
1256
+ // 'allowedRemoteDirectories' to opt into an allowlist for this server.
1257
+ if (config.disableSftpPathPolicy || allowedRoots.length === 0) {
1258
+ return normalized;
1264
1259
  }
1265
1260
  const isAllowed = allowedRoots.some((root) => normalized === root || normalized.startsWith(root === "/" ? "/" : root + "/"));
1266
1261
  if (!isAllowed) {
@@ -7,8 +7,8 @@ import { formatToolErrorResponse, toToolError } from "../utils/tool-error.js";
7
7
  */
8
8
  export function registerDownloadTool(server) {
9
9
  const sshManager = SSHConnectionManager.getInstance();
10
- server.tool("download", "Download a file from the remote server to the MCP host over SFTP. Use this when you need to inspect or save a remote file locally. The remote source must live inside one of the server's allowedRemoteDirectories; call show-whitelist to discover the allowed paths.", {
11
- remotePath: z.string().describe("Path to the file on the remote server. Must be an absolute POSIX path inside one of the server's allowedRemoteDirectories (e.g. /var/log/app.log)."),
10
+ server.tool("download", "Download a file from the remote server to the MCP host over SFTP. Use this when you need to inspect or save a remote file locally. By default any absolute remote path is allowed; if the server configures allowedRemoteDirectories, the source must live inside one of those entries call show-whitelist to check.", {
11
+ remotePath: z.string().describe("Path to the file on the remote server. Must be an absolute POSIX path (e.g. /var/log/app.log); restricted to allowedRemoteDirectories only if the server configures that list."),
12
12
  localPath: z.string().describe("Destination path on the MCP host. Must be inside the MCP working directory or one of the server's allowedLocalDirectories."),
13
13
  connectionName: z.string().optional().describe("Target server name from list-servers. Required when multiple servers are enabled; optional when only one server is enabled."),
14
14
  }, async ({ remotePath, localPath, connectionName }) => {
@@ -100,15 +100,18 @@ export function registerShowWhitelistTool(server) {
100
100
  const allowedLocalDirs = config.allowedLocalDirectories ?? [];
101
101
  const mcpCwd = process.cwd();
102
102
  output += `## 📂 SFTP Path Policy (upload / download / transfer)\n\n`;
103
+ if (config.disableSftpPathPolicy) {
104
+ output += `**\`disableSftpPathPolicy\` is set — all path containment checks below are bypassed.** ` +
105
+ `Any absolute remote path and any local path is allowed for upload/download/transfer.\n\n`;
106
+ }
103
107
  // ----- Remote directories -----
104
108
  output += `### Allowed remote directories\n\n`;
105
- if (allowedRemoteDirs.length === 0) {
106
- output += `⚠️ **\`allowedRemoteDirectories\` is NOT configured.** ` +
107
- `SFTP upload/download/transfer is **disabled** for this server every call will be rejected with \`REMOTE_PATH_NOT_ALLOWED\`. ` +
108
- `To enable file transfer, add absolute POSIX directories under \`allowedRemoteDirectories\` in the server's YAML config.\n\n`;
109
+ if (config.disableSftpPathPolicy || allowedRemoteDirs.length === 0) {
110
+ output += `\`allowedRemoteDirectories\` is not configured, so **any absolute POSIX path is allowed** (default is open). ` +
111
+ `To restrict SFTP to specific directories, add them under \`allowedRemoteDirectories\` in the server's YAML config.\n\n`;
109
112
  }
110
113
  else {
111
- output += `${allowedRemoteDirs.length} entr${allowedRemoteDirs.length === 1 ? "y" : "ies"}:\n\n`;
114
+ output += `${allowedRemoteDirs.length} entr${allowedRemoteDirs.length === 1 ? "y" : "ies"} (restricting SFTP to these paths only):\n\n`;
112
115
  for (const dir of allowedRemoteDirs) {
113
116
  output += `- \`${dir}\`\n`;
114
117
  }
@@ -116,24 +119,31 @@ export function registerShowWhitelistTool(server) {
116
119
  }
117
120
  // ----- Local directories -----
118
121
  output += `### Allowed local directories\n\n`;
119
- output += `Always implicitly allowed: \`${mcpCwd}\` _(the MCP working directory)_\n\n`;
120
- if (allowedLocalDirs.length === 0) {
121
- output += `⚠️ **\`allowedLocalDirectories\` is NOT configured.** ` +
122
- `Only paths inside the MCP working directory above can be used as the local side of upload/download — any other path will be rejected with \`LOCAL_PATH_NOT_ALLOWED\`. ` +
123
- `To allow more locations, add absolute host paths under \`allowedLocalDirectories\` in the server's YAML config.\n\n`;
122
+ if (config.disableSftpPathPolicy) {
123
+ output += `\`disableSftpPathPolicy\` is set, so **any local path is allowed**.\n\n`;
124
124
  }
125
125
  else {
126
- output += `Additional entries (${allowedLocalDirs.length}):\n\n`;
127
- for (const dir of allowedLocalDirs) {
128
- output += `- \`${dir}\`\n`;
126
+ output += `Always implicitly allowed: \`${mcpCwd}\` _(the MCP working directory)_\n\n`;
127
+ if (allowedLocalDirs.length === 0) {
128
+ output += `\`allowedLocalDirectories\` is not configured. ` +
129
+ `Only paths inside the MCP working directory above can be used as the local side of upload/download — any other path will be rejected with \`LOCAL_PATH_NOT_ALLOWED\`. ` +
130
+ `To allow more locations, add absolute host paths under \`allowedLocalDirectories\`, or set \`disableSftpPathPolicy: true\` to allow any local path.\n\n`;
131
+ }
132
+ else {
133
+ output += `Additional entries (${allowedLocalDirs.length}):\n\n`;
134
+ for (const dir of allowedLocalDirs) {
135
+ output += `- \`${dir}\`\n`;
136
+ }
137
+ output += `\n`;
129
138
  }
130
- output += `\n`;
131
139
  }
132
140
  // ----- Matching rules -----
133
141
  output += `### Matching rules\n\n`;
134
- output += `- A path is allowed iff it equals an entry above, or starts with \`<entry> + separator\`.\n`;
135
- output += `- Remote paths must be absolute POSIX (\`/...\`); \`..\` segments and null bytes are rejected up-front.\n`;
136
- output += `- Local paths are resolved on the MCP host first, then matched.\n`;
142
+ output += `- Default is open: with no \`allowedRemoteDirectories\` configured, any absolute POSIX remote path is allowed.\n`;
143
+ output += `- Configuring \`allowedRemoteDirectories\` opts into an allowlist a path is then allowed iff it equals an entry, or starts with \`<entry> + separator\`.\n`;
144
+ output += `- \`disableSftpPathPolicy: true\` bypasses both the remote allowlist and the local directory check entirely.\n`;
145
+ output += `- Remote paths must be absolute POSIX (\`/...\`); \`..\` segments and null bytes are rejected up-front regardless of policy.\n`;
146
+ output += `- Local paths are resolved on the MCP host first, then matched (unless \`disableSftpPathPolicy\` is set).\n`;
137
147
  output += `- These path lists do **not** affect \`execute-command\`. They only restrict SFTP file transfers.\n\n`;
138
148
  // ----- Output log policy (execute-command full-output persistence) -----
139
149
  const outputLogRoot = sshManager.getOutputLogRoot();
@@ -26,12 +26,12 @@ Set recursive=true when transferring a directory (upload/download only).
26
26
  For relay mode, specify sourceServer, sourceRemotePath, destServer, destRemotePath.`, {
27
27
  mode: z.enum(["upload", "download", "relay"]).describe("'upload' pushes local → remote. 'download' pulls remote → local. 'relay' copies remote-A → remote-B through the MCP host."),
28
28
  localPath: z.string().optional().describe("(upload/download only) Path on the MCP host. Must be inside the MCP working directory or one of the server's allowedLocalDirectories."),
29
- remotePath: z.string().optional().describe("(upload/download only) Absolute POSIX path on the remote server. Must live inside one of the server's allowedRemoteDirectories — call show-whitelist to discover the allowed paths."),
29
+ remotePath: z.string().optional().describe("(upload/download only) Absolute POSIX path on the remote server. Any path is allowed by default; restricted to allowedRemoteDirectories only if the server configures that list — call show-whitelist to check."),
30
30
  connectionName: z.string().optional().describe("(upload/download only) Target server name from list-servers. Required when multiple servers are enabled."),
31
31
  sourceServer: z.string().optional().describe("(relay only) Server name to download the file from."),
32
- sourceRemotePath: z.string().optional().describe("(relay only) Absolute POSIX file path on the source server. Must live inside the source server's allowedRemoteDirectories."),
32
+ sourceRemotePath: z.string().optional().describe("(relay only) Absolute POSIX file path on the source server. Any path is allowed by default unless the source server configures allowedRemoteDirectories."),
33
33
  destServer: z.string().optional().describe("(relay only) Server name to upload the file to."),
34
- destRemotePath: z.string().optional().describe("(relay only) Absolute POSIX destination path on the target server. Must live inside the destination server's allowedRemoteDirectories."),
34
+ destRemotePath: z.string().optional().describe("(relay only) Absolute POSIX destination path on the target server. Any path is allowed by default unless the destination server configures allowedRemoteDirectories."),
35
35
  recursive: z.boolean().optional().describe("(upload/download only) When true, transfers an entire directory tree recursively. Default false."),
36
36
  skipIfIdentical: z.boolean().optional().describe("When true (default), skip the transfer if the destination already matches the source. " +
37
37
  "Upload: byte-compare for files \u2264 256 MiB, MD5 otherwise; shell scripts (.sh / .bash / .zsh) ignore CRLF\u2194LF differences. " +
@@ -7,11 +7,11 @@ import { formatToolErrorResponse, toToolError } from "../utils/tool-error.js";
7
7
  */
8
8
  export function registerUploadTool(server) {
9
9
  const sshManager = SSHConnectionManager.getInstance();
10
- server.tool("upload", "Upload a local file from the MCP host to the remote server over SFTP. Use this when the file already exists locally and must be copied to the selected SSH server. The remote destination must live inside one of the server's allowedRemoteDirectories; call show-whitelist to discover the allowed paths. " +
10
+ server.tool("upload", "Upload a local file from the MCP host to the remote server over SFTP. Use this when the file already exists locally and must be copied to the selected SSH server. By default any absolute remote path is allowed; if the server configures allowedRemoteDirectories, the destination must live inside one of those entries call show-whitelist to check. " +
11
11
  "Shell scripts (.sh / .bash / .zsh) with CRLF line endings are auto-converted to LF before upload (the response notes when this happens). " +
12
12
  "By default the upload is skipped if the remote file is already identical to the local one (byte-compare for files \u2264 256 MiB, MD5 otherwise; shell scripts are compared in a line-ending-agnostic way so CRLF\u2194LF differences alone do not trigger a re-upload) \u2014 pass skipIfIdentical=false to force a re-upload.", {
13
13
  localPath: z.string().describe("Path to a local file on the MCP host. Must be inside the MCP working directory or one of the server's allowedLocalDirectories."),
14
- remotePath: z.string().describe("Destination path on the remote server. Must be an absolute POSIX path inside one of the server's allowedRemoteDirectories (e.g. /home/user/uploads/file.txt)."),
14
+ remotePath: z.string().describe("Destination path on the remote server. Must be an absolute POSIX path (e.g. /home/user/uploads/file.txt); restricted to allowedRemoteDirectories only if the server configures that list."),
15
15
  connectionName: z.string().optional().describe("Target server name from list-servers. Required when multiple servers are enabled; optional when only one server is enabled."),
16
16
  skipIfIdentical: z.boolean().optional().describe("When true (default), skip the upload if the remote file is already identical (byte-compare for files \u2264 256 MiB, MD5 otherwise; shell scripts ignore CRLF\u2194LF differences). Set to false to force re-upload."),
17
17
  }, async ({ localPath, remotePath, connectionName, skipIfIdentical }) => {
package/package.json CHANGED
@@ -1,61 +1,61 @@
1
- {
2
- "name": "@aaarc/handfree-ssh-mcp",
3
- "version": "1.0.3",
4
- "description": "Handfree SSH MCP Server - A hands-free SSH automation tool via MCP protocol",
5
- "main": "build/index.js",
6
- "type": "module",
7
- "bin": {
8
- "handfree-ssh-mcp": "build/index.js"
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",
18
- "publishConfig": {
19
- "access": "public"
20
- },
21
- "scripts": {
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",
23
- "test:config": "npm run build && node --test build/tests/config.test.js",
24
- "test:cmd": "npm run build && node --test build/tests/command-validation.test.js",
25
- "test:tools": "npm run build && node --test build/tests/tools.test.js",
26
- "test:integration": "npm run build && node build/tests/integration.test.js",
27
- "test:all": "npm run test && npm run test:integration",
28
- "build": "node scripts/build.js",
29
- "prepublishOnly": "npm run build"
30
- },
31
- "keywords": [
32
- "ssh",
33
- "mcp",
34
- "server",
35
- "cli"
36
- ],
37
- "author": "Junki",
38
- "license": "ISC",
39
- "dependencies": {
40
- "@modelcontextprotocol/sdk": "1.17.5",
41
- "js-yaml": "^4.1.0",
42
- "socks": "^2.8.6",
43
- "ssh2": "^1.16.0",
44
- "zod": "^3.24.2"
45
- },
46
- "devDependencies": {
47
- "@types/js-yaml": "^4.0.9",
48
- "@types/node": "^22.13.10",
49
- "@types/ssh2": "^1.15.5",
50
- "typescript": "^5.8.2"
51
- },
52
- "files": [
53
- "build/config/**/*",
54
- "build/core/**/*",
55
- "build/models/**/*",
56
- "build/services/**/*",
57
- "build/tools/**/*",
58
- "build/utils/**/*",
59
- "build/index.js"
60
- ]
61
- }
1
+ {
2
+ "name": "@aaarc/handfree-ssh-mcp",
3
+ "version": "1.0.5",
4
+ "description": "Handfree SSH MCP Server - A hands-free SSH automation tool via MCP protocol",
5
+ "main": "build/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "handfree-ssh-mcp": "build/index.js"
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",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
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",
23
+ "test:config": "npm run build && node --test build/tests/config.test.js",
24
+ "test:cmd": "npm run build && node --test build/tests/command-validation.test.js",
25
+ "test:tools": "npm run build && node --test build/tests/tools.test.js",
26
+ "test:integration": "npm run build && node build/tests/integration.test.js",
27
+ "test:all": "npm run test && npm run test:integration",
28
+ "build": "node scripts/build.js",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "ssh",
33
+ "mcp",
34
+ "server",
35
+ "cli"
36
+ ],
37
+ "author": "Junki",
38
+ "license": "ISC",
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "1.17.5",
41
+ "js-yaml": "^4.1.0",
42
+ "socks": "^2.8.6",
43
+ "ssh2": "^1.16.0",
44
+ "zod": "^3.24.2"
45
+ },
46
+ "devDependencies": {
47
+ "@types/js-yaml": "^4.0.9",
48
+ "@types/node": "^22.13.10",
49
+ "@types/ssh2": "^1.15.5",
50
+ "typescript": "^5.8.2"
51
+ },
52
+ "files": [
53
+ "build/config/**/*",
54
+ "build/core/**/*",
55
+ "build/models/**/*",
56
+ "build/services/**/*",
57
+ "build/tools/**/*",
58
+ "build/utils/**/*",
59
+ "build/index.js"
60
+ ]
61
+ }