@aaarc/handfree-ssh-mcp 1.0.5 → 1.0.7

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.
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { SSHConnectionManager } from "../services/ssh-connection-manager.js";
3
+ import { Logger } from "../utils/logger.js";
4
+ import { formatToolErrorResponse, toToolError } from "../utils/tool-error.js";
5
+ /**
6
+ * Register close-connection tool
7
+ */
8
+ export function registerCloseConnectionTool(server) {
9
+ const sshManager = SSHConnectionManager.getInstance();
10
+ server.tool("close-connection", "Close the cached SSH connection for a configured server. Use this after a timeout, stale connection suspicion, or before retrying a host with a clean cached connection. This affects only cached/reused SSH clients; reuseConnection=false command clients already close after each command. Closing a jump host also closes cached target connections whose jump chain uses that host.", {
11
+ connectionName: z
12
+ .string()
13
+ .optional()
14
+ .describe("Target server name from list-servers. Required when multiple servers are enabled; optional when only one server is enabled."),
15
+ }, async ({ connectionName }) => {
16
+ try {
17
+ const result = sshManager.closeConnection(connectionName);
18
+ return {
19
+ content: [
20
+ {
21
+ type: "text",
22
+ text: JSON.stringify(result),
23
+ },
24
+ ],
25
+ };
26
+ }
27
+ catch (error) {
28
+ const toolError = toToolError(error, "INVALID_CONFIGURATION");
29
+ Logger.handleError(toolError, "Failed to close SSH connection");
30
+ return {
31
+ content: [{ type: "text", text: formatToolErrorResponse(toolError) }],
32
+ isError: true,
33
+ };
34
+ }
35
+ });
36
+ }
@@ -11,10 +11,23 @@ export function registerDownloadTool(server) {
11
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
- }, async ({ remotePath, localPath, connectionName }) => {
14
+ reuseConnection: z.boolean().optional().describe("Default true. Reuse the cached SSH connection for SFTP. Set false after a timeout or suspected stale cached SSH connection to force a fresh TCP/SSH connection for this transfer; the fresh connection closes afterwards."),
15
+ timeout: z.number().positive().optional().describe("Timeout in ms for SSH setup and SFTP channel opening. Transfer stream duration itself is not forcibly interrupted by this option."),
16
+ vvv: z.boolean().optional().describe("Default false. Append bounded SSH/SFTP debug output. For fresh ssh2 handshake logs, also set reuseConnection=false."),
17
+ fast: z.boolean().optional().describe("Default false. When true, use ssh2 fastGet for a single-file download, which performs parallel SFTP reads for better throughput."),
18
+ sftpConcurrency: z.number().int().positive().optional().describe("Only used when fast=true. Number of concurrent SFTP chunks for ssh2 fastGet; omitted uses ssh2's default."),
19
+ chunkSize: z.number().int().positive().optional().describe("Only used when fast=true. Chunk size in bytes for ssh2 fastGet; omitted uses ssh2's default."),
20
+ }, async ({ remotePath, localPath, connectionName, reuseConnection, timeout, vvv, fast, sftpConcurrency, chunkSize }) => {
15
21
  try {
16
22
  const resolvedName = sshManager.resolveServer(connectionName);
17
- const result = await sshManager.download(remotePath, localPath, resolvedName);
23
+ const result = await sshManager.download(remotePath, localPath, resolvedName, {
24
+ reuseConnection,
25
+ timeout,
26
+ vvv,
27
+ fast,
28
+ sftpConcurrency,
29
+ chunkSize,
30
+ });
18
31
  return {
19
32
  content: [{ type: "text", text: result }],
20
33
  };
@@ -11,7 +11,7 @@ import { formatToolErrorResponse, toToolError } from "../utils/tool-error.js";
11
11
  */
12
12
  export function registerExecuteCommandTool(server) {
13
13
  const sshManager = SSHConnectionManager.getInstance();
14
- server.tool("execute-command", "Execute a shell command on a remote server over SSH. Use this for command-line actions on the selected host. Streaming mode is enabled by default so long-running commands can emit progress; set stream=false for short commands where you only want the final output. The returned text is tail-only-capped at maxOutputBytes PER STREAM (default 65536 bytes for stdout and 65536 bytes for stderr, so the response can be up to ~2x that); the FULL stdout/stderr is always persisted to a local log file under <cwd>/.handfree-output/<server>/<user>/, and the response includes that path whenever output was truncated.", {
14
+ server.tool("execute-command", "Execute a shell command on a remote server over SSH. Use this for command-line actions on the selected host. Streaming mode is enabled by default so long-running commands can emit progress; set stream=false for short commands where you only want the final output. SSH connections are reused by default for speed; if an execute-command call times out or you suspect a stale cached SSH connection, retry with reuseConnection=false to force a fresh TCP/SSH connection for that command. Set vvv=true only when debugging SSH/channel issues; with reuseConnection=false it includes ssh2 handshake/debug lines in the result or error. The returned text is tail-only-capped at maxOutputBytes PER STREAM (default 65536 bytes for stdout and 65536 bytes for stderr, so the response can be up to ~2x that); the FULL stdout/stderr is always persisted to a local log file under <cwd>/.handfree-output/<server>/<user>/, and the response includes that path whenever output was truncated.", {
15
15
  cmdString: z.string().describe("Exact remote shell command to run. Prefer a single command per call, for example 'pwd', 'ls -la', 'cat /etc/hostname', or 'git status'. Compound commands may be blocked by command policy even if each subcommand is safe."),
16
16
  connectionName: z
17
17
  .string()
@@ -20,18 +20,26 @@ export function registerExecuteCommandTool(server) {
20
20
  timeout: z
21
21
  .number()
22
22
  .optional()
23
- .describe("Maximum runtime in milliseconds. Defaults to 300000 when stream=true and 30000 when stream=false. Increase this for long-running commands; reduce it for fast probes."),
23
+ .describe("Timeout in milliseconds applied separately to SSH connection setup, exec-channel opening, and remote command execution for each attempt. Defaults to 300000 when stream=true and 30000 when stream=false. Increase this for long-running commands; reduce it for fast probes."),
24
24
  stream: z
25
25
  .boolean()
26
26
  .optional()
27
27
  .describe("Whether to stream progress output. Default is true. Use false for short commands like pwd, ls, cat, head, tail, or git status when you only need the final result."),
28
+ reuseConnection: z
29
+ .boolean()
30
+ .optional()
31
+ .describe("Whether to reuse the cached SSH connection for this server. Default is true for speed. Set false after a timeout or suspected stale/bad cached connection; false opens a fresh SSH connection for this command and closes it afterwards."),
32
+ vvv: z
33
+ .boolean()
34
+ .optional()
35
+ .describe("Default false. When true, append bounded SSH/channel debug output to the result or error. For full ssh2 handshake debug, also set reuseConnection=false; already-reused cached clients cannot retroactively emit handshake logs."),
28
36
  maxOutputBytes: z
29
37
  .number()
30
38
  .int()
31
39
  .nonnegative()
32
40
  .optional()
33
41
  .describe("Per-stream cap on bytes returned to the caller (tail-only). Applied independently to stdout and stderr, so the combined response can be up to ~2x this value. Defaults to 65536 (64 KiB). The full output is always saved to disk regardless; only the returned text is trimmed. Set higher when you need more context, lower to save tokens."),
34
- }, async ({ cmdString, connectionName, timeout, stream, maxOutputBytes }, extra) => {
42
+ }, async ({ cmdString, connectionName, timeout, stream, reuseConnection, vvv, maxOutputBytes }, extra) => {
35
43
  try {
36
44
  const resolvedName = sshManager.resolveServer(connectionName);
37
45
  const useStream = stream !== false;
@@ -54,6 +62,8 @@ export function registerExecuteCommandTool(server) {
54
62
  const result = await sshManager.executeCommandWithProgress(cmdString, resolvedName, {
55
63
  timeout: timeout || 300000,
56
64
  maxOutputBytes,
65
+ reuseConnection,
66
+ vvv,
57
67
  onProgress,
58
68
  });
59
69
  return {
@@ -63,6 +73,8 @@ export function registerExecuteCommandTool(server) {
63
73
  const result = await sshManager.executeCommand(cmdString, resolvedName, {
64
74
  timeout: timeout || 30000,
65
75
  maxOutputBytes,
76
+ reuseConnection,
77
+ vvv,
66
78
  });
67
79
  return {
68
80
  content: [{ type: "text", text: result }],
@@ -18,19 +18,30 @@ Example:
18
18
  Parameters:
19
19
  cmdString (string, required) The shell command to execute.
20
20
  connectionName (string, see below) Target server name from list-servers.
21
- stream (boolean, optional) Default true. Set false for short
22
- commands when you only need the final output.
23
- timeout (number, optional) Max runtime in ms.
24
- Defaults: 300000 (stream) / 30000 (non-stream).
21
+ stream (boolean, optional) Default true. Set false for short
22
+ commands when you only need the final output.
23
+ reuseConnection (boolean, optional) Default true. Set false after a timeout
24
+ or suspected stale cached SSH connection to force a fresh
25
+ TCP/SSH connection for this command.
26
+ vvv (boolean, optional) Default false. Append bounded
27
+ SSH/channel debug output. Use with reuseConnection=false
28
+ when you need fresh handshake logs.
29
+ timeout (number, optional) Per-attempt phase timeout in ms:
30
+ SSH setup, exec-channel open, and
31
+ remote command execution each use this
32
+ cap.
33
+ Defaults: 300000 (stream) / 30000 (non-stream).
25
34
 
26
35
  connectionName rule:
27
36
  • If only one server is enabled → optional (auto-selected).
28
37
  • If multiple servers are enabled → REQUIRED.
29
38
 
30
39
  Examples:
31
- execute-command { cmdString: "pwd" }
32
- execute-command { cmdString: "docker ps -a", connectionName: "prod", stream: false }
33
- execute-command { cmdString: "tail -f /var/log/syslog", stream: true, timeout: 600000 }`,
40
+ execute-command { cmdString: "pwd" }
41
+ execute-command { cmdString: "docker ps -a", connectionName: "prod", stream: false }
42
+ execute-command { cmdString: "tail -f /var/log/syslog", stream: true, timeout: 600000 }
43
+ execute-command { cmdString: "hostname", connectionName: "scnet", stream: false, reuseConnection: false }
44
+ execute-command { cmdString: "hostname", connectionName: "scnet", stream: false, reuseConnection: false, vvv: true }`,
34
45
  "show-whitelist": `show-whitelist — Show the active command policy for a server.
35
46
 
36
47
  Parameters:
@@ -41,34 +52,75 @@ connectionName rule:
41
52
  • If multiple servers are enabled → REQUIRED.
42
53
 
43
54
  Returns: Command mode, built-in blacklist, configured whitelist/blacklist patterns, and example commands when whitelist mode is active.`,
44
- "upload": `uploadUpload a single local file to a remote server over SFTP.
55
+ "close-connection": `close-connectionClose a cached SSH connection.
56
+
57
+ Parameters:
58
+ connectionName (string, see below) Target server name from list-servers.
59
+
60
+ connectionName rule:
61
+ • If only one server is enabled → optional (auto-selected).
62
+ • If multiple servers are enabled → REQUIRED.
63
+
64
+ Behavior:
65
+ • Closes the cached/reused SSH client for the target server.
66
+ • Closing a jump host also closes cached targets whose jump chain uses it.
67
+ • Does not affect reuseConnection=false commands, because those one-shot
68
+ connections already close after each command.
69
+
70
+ Examples:
71
+ close-connection { connectionName: "scnet" }
72
+ close-connection { connectionName: "dcu" }`,
73
+ "upload": `upload — Upload a single local file to a remote server over SFTP.
45
74
 
46
75
  Parameters:
47
76
  localPath (string, required) File path on the MCP host.
48
77
  Must be inside the MCP working directory.
49
- remotePath (string, required) Destination path on the remote server.
50
- connectionName (string, see below) Target server name from list-servers.
51
-
52
- connectionName rule:
53
- If only one server is enabled → optional (auto-selected).
54
- If multiple servers are enabled → REQUIRED.
55
-
56
- Example:
57
- upload { localPath: "data.csv", remotePath: "/tmp/data.csv" }`,
78
+ remotePath (string, required) Destination path on the remote server.
79
+ connectionName (string, see below) Target server name from list-servers.
80
+ skipIfIdentical (boolean, optional) Default true. Skip when remote matches.
81
+ reuseConnection (boolean, optional) Default true. Set false after timeout
82
+ or suspected stale cached SSH connection.
83
+ timeout (number, optional) SSH setup and SFTP channel-open timeout.
84
+ vvv (boolean, optional) Default false. Append bounded SSH/SFTP debug.
85
+ Recursive success returns structured JSON; debug is surfaced
86
+ for single-file results, relay results, and recursive errors.
87
+ fast (boolean, optional) Default false. Use ssh2 fastPut for
88
+ single-file upload throughput. Not multi-file concurrency.
89
+ sftpConcurrency (number, optional) Only with fast=true. Concurrent SFTP chunks.
90
+ chunkSize (number, optional) Only with fast=true. SFTP chunk bytes.
91
+
92
+ connectionName rule:
93
+ • If only one server is enabled → optional (auto-selected).
94
+ • If multiple servers are enabled → REQUIRED.
95
+
96
+ Example:
97
+ upload { localPath: "data.csv", remotePath: "/tmp/data.csv" }
98
+ upload { localPath: "big.bin", remotePath: "/tmp/big.bin", fast: true,
99
+ sftpConcurrency: 32, chunkSize: 131072 }`,
58
100
  "download": `download — Download a single file from a remote server over SFTP.
59
101
 
60
102
  Parameters:
61
- remotePath (string, required) File path on the remote server.
62
- localPath (string, required) Destination on the MCP host.
63
- Must be inside the MCP working directory.
64
- connectionName (string, see below) Target server name from list-servers.
65
-
66
- connectionName rule:
67
- If only one server is enabled → optional (auto-selected).
68
- If multiple servers are enabled → REQUIRED.
69
-
70
- Example:
71
- download { remotePath: "/var/log/app.log", localPath: "app.log" }`,
103
+ remotePath (string, required) File path on the remote server.
104
+ localPath (string, required) Destination on the MCP host.
105
+ Must be inside the MCP working directory.
106
+ connectionName (string, see below) Target server name from list-servers.
107
+ reuseConnection (boolean, optional) Default true. Set false after timeout
108
+ or suspected stale cached SSH connection.
109
+ timeout (number, optional) SSH setup and SFTP channel-open timeout.
110
+ vvv (boolean, optional) Default false. Append bounded SSH/SFTP debug.
111
+ fast (boolean, optional) Default false. Use ssh2 fastGet for
112
+ single-file download throughput. Not multi-file concurrency.
113
+ sftpConcurrency (number, optional) Only with fast=true. Concurrent SFTP chunks.
114
+ chunkSize (number, optional) Only with fast=true. SFTP chunk bytes.
115
+
116
+ connectionName rule:
117
+ • If only one server is enabled → optional (auto-selected).
118
+ • If multiple servers are enabled → REQUIRED.
119
+
120
+ Example:
121
+ download { remotePath: "/var/log/app.log", localPath: "app.log" }
122
+ download { remotePath: "/tmp/big.bin", localPath: "big.bin", fast: true,
123
+ sftpConcurrency: 32, chunkSize: 131072 }`,
72
124
  "transfer": `transfer — Move files between hosts (single/recursive/cross-server).
73
125
 
74
126
  Modes:
@@ -83,15 +135,28 @@ Parameters for upload / download:
83
135
  mode (string, required) "upload" or "download"
84
136
  localPath (string, required) Path on the MCP host.
85
137
  remotePath (string, required) Path on the remote server.
86
- connectionName (string, see below) Target server name.
87
- recursive (boolean, optional) True to transfer a whole directory tree.
88
-
89
- Parameters for relay:
138
+ connectionName (string, see below) Target server name.
139
+ recursive (boolean, optional) True to transfer a whole directory tree.
140
+ reuseConnection (boolean, optional) Default true. Set false after timeout.
141
+ timeout (number, optional) SSH setup and SFTP channel-open timeout.
142
+ vvv (boolean, optional) Default false. Append bounded SSH/SFTP debug.
143
+ fast (boolean, optional) Default false. upload/download only:
144
+ use ssh2 fastPut/fastGet for each single file. Directory
145
+ recursion stays sequential; no multi-file concurrency.
146
+ sftpConcurrency (number, optional) Only with fast=true. Concurrent SFTP chunks.
147
+ chunkSize (number, optional) Only with fast=true. SFTP chunk bytes.
148
+
149
+ Parameters for relay:
90
150
  mode (string, required) "relay"
91
151
  sourceServer (string, required) Server name to read from.
92
- sourceRemotePath (string, required) File path on source server.
93
- destServer (string, required) Server name to write to.
94
- destRemotePath (string, required) File path on dest server.
152
+ sourceRemotePath (string, required) File path on source server.
153
+ destServer (string, required) Server name to write to.
154
+ destRemotePath (string, required) File path on dest server.
155
+ reuseConnection (boolean, optional) Default true. Set false after timeout.
156
+ timeout (number, optional) SSH setup and SFTP channel-open timeout.
157
+ vvv (boolean, optional) Default false. Append bounded SSH/SFTP debug.
158
+ fast (boolean, optional) Accepted but relay keeps the streaming
159
+ SFTP pipe path; fastGet/fastPut apply to host<->remote only.
95
160
 
96
161
  connectionName rule (upload/download):
97
162
  • If only one server is enabled → optional.
@@ -113,18 +178,20 @@ Example:
113
178
  };
114
179
  const TOOL_OVERVIEW = `Available tools (use help { tool: "<name>" } for details):
115
180
 
116
- list-servers Discover available SSH servers and their status.
117
- execute-command Run a shell command on a remote server.
181
+ list-servers Discover available SSH servers and their status.
182
+ execute-command Run a shell command on a remote server.
118
183
  show-whitelist Show the active command policy.
119
- upload Upload a single file to a remote server.
120
- download Download a single file from a remote server.
184
+ close-connection Close a cached SSH connection for a server.
185
+ upload Upload a single file to a remote server.
186
+ download Download a single file from a remote server.
121
187
  transfer Move files: single, recursive, or cross-server relay.
122
188
  help Show this help or detailed per-tool usage.
123
189
 
124
190
  Quick start:
125
- 1. list-servers → discover server names
191
+ 1. list-servers → discover server names
126
192
  2. show-whitelist { connectionName: "<name>" } → inspect command policy
127
- 3. execute-command { cmdString: "pwd", connectionName: "<name>" }`;
193
+ 3. execute-command { cmdString: "pwd", connectionName: "<name>" }
194
+ 4. close-connection { connectionName: "<name>" } → drop a stale cached SSH client`;
128
195
  /**
129
196
  * Register help tool
130
197
  */
@@ -3,6 +3,7 @@ import { registerUploadTool } from "./upload.js";
3
3
  import { registerDownloadTool } from "./download.js";
4
4
  import { registerListServersTool } from "./list-servers.js";
5
5
  import { registerShowWhitelistTool } from "./show-whitelist.js";
6
+ import { registerCloseConnectionTool } from "./close-connection.js";
6
7
  import { registerTransferTool } from "./transfer.js";
7
8
  import { registerHelpTool } from "./help.js";
8
9
  /**
@@ -15,6 +16,7 @@ export function registerAllTools(server) {
15
16
  registerDownloadTool(server);
16
17
  registerListServersTool(server);
17
18
  registerShowWhitelistTool(server);
19
+ registerCloseConnectionTool(server);
18
20
  registerTransferTool(server);
19
21
  registerHelpTool(server);
20
22
  }
@@ -37,22 +37,28 @@ For relay mode, specify sourceServer, sourceRemotePath, destServer, destRemotePa
37
37
  "Upload: byte-compare for files \u2264 256 MiB, MD5 otherwise; shell scripts (.sh / .bash / .zsh) ignore CRLF\u2194LF differences. " +
38
38
  "Relay: size match + md5sum match on both servers (when available); falls back to transferring if md5sum is missing on either side. " +
39
39
  "Download is never skipped. Set to false to force the transfer."),
40
+ reuseConnection: z.boolean().optional().describe("Default true. Reuse cached SSH connection(s) for SFTP. Set false after a timeout or suspected stale cached connection to force fresh TCP/SSH connection(s) for this transfer; fresh connections close afterwards."),
41
+ timeout: z.number().positive().optional().describe("Timeout in ms for SSH setup and SFTP channel opening. Transfer stream duration itself is not forcibly interrupted by this option."),
42
+ vvv: z.boolean().optional().describe("Default false. Append bounded SSH/SFTP debug output for single-file and relay results, and for recursive errors. For fresh ssh2 handshake logs, also set reuseConnection=false."),
43
+ fast: z.boolean().optional().describe("Default false. Upload/download only: use ssh2 fastPut/fastGet for single files, with parallel SFTP chunks for better throughput. Relay mode keeps the streaming pipe path."),
44
+ sftpConcurrency: z.number().int().positive().optional().describe("Only used when fast=true for upload/download. Number of concurrent SFTP chunks; omitted uses ssh2's default."),
45
+ chunkSize: z.number().int().positive().optional().describe("Only used when fast=true for upload/download. Chunk size in bytes; omitted uses ssh2's default."),
40
46
  }, async (params) => {
41
47
  try {
42
48
  const { mode } = params;
43
49
  if (mode === "relay") {
44
- const { sourceServer, sourceRemotePath, destServer, destRemotePath, skipIfIdentical } = params;
50
+ const { sourceServer, sourceRemotePath, destServer, destRemotePath, skipIfIdentical, reuseConnection, timeout, vvv } = params;
45
51
  if (!sourceServer || !sourceRemotePath || !destServer || !destRemotePath) {
46
52
  return {
47
53
  content: [{ type: "text", text: "relay mode requires: sourceServer, sourceRemotePath, destServer, destRemotePath" }],
48
54
  isError: true,
49
55
  };
50
56
  }
51
- const result = await sshManager.transferBetweenServers(sourceServer, sourceRemotePath, destServer, destRemotePath, { skipIfIdentical: skipIfIdentical !== false });
57
+ const result = await sshManager.transferBetweenServers(sourceServer, sourceRemotePath, destServer, destRemotePath, { skipIfIdentical: skipIfIdentical !== false, reuseConnection, timeout, vvv });
52
58
  return { content: [{ type: "text", text: result }] };
53
59
  }
54
60
  // upload or download
55
- const { localPath, remotePath, connectionName, recursive, skipIfIdentical } = params;
61
+ const { localPath, remotePath, connectionName, recursive, skipIfIdentical, reuseConnection, timeout, vvv, fast, sftpConcurrency, chunkSize } = params;
56
62
  if (!localPath || !remotePath) {
57
63
  return {
58
64
  content: [{ type: "text", text: `${mode} mode requires: localPath, remotePath` }],
@@ -60,14 +66,15 @@ For relay mode, specify sourceServer, sourceRemotePath, destServer, destRemotePa
60
66
  };
61
67
  }
62
68
  const resolvedName = sshManager.resolveServer(connectionName);
63
- const uploadOptions = { skipIfIdentical: skipIfIdentical !== false };
69
+ const sftpOptions = { reuseConnection, timeout, vvv, fast, sftpConcurrency, chunkSize };
70
+ const uploadOptions = { skipIfIdentical: skipIfIdentical !== false, ...sftpOptions };
64
71
  if (recursive) {
65
72
  let files;
66
73
  if (mode === "upload") {
67
74
  files = await sshManager.uploadDirectory(localPath, remotePath, resolvedName, uploadOptions);
68
75
  }
69
76
  else {
70
- files = await sshManager.downloadDirectory(remotePath, localPath, resolvedName);
77
+ files = await sshManager.downloadDirectory(remotePath, localPath, resolvedName, sftpOptions);
71
78
  }
72
79
  const summary = `Recursive ${mode} complete. ${files.length} file(s) transferred.`;
73
80
  return {
@@ -80,7 +87,7 @@ For relay mode, specify sourceServer, sourceRemotePath, destServer, destRemotePa
80
87
  return { content: [{ type: "text", text: result }] };
81
88
  }
82
89
  else {
83
- const result = await sshManager.download(remotePath, localPath, resolvedName);
90
+ const result = await sshManager.download(remotePath, localPath, resolvedName, sftpOptions);
84
91
  return { content: [{ type: "text", text: result }] };
85
92
  }
86
93
  }
@@ -14,11 +14,23 @@ export function registerUploadTool(server) {
14
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
- }, async ({ localPath, remotePath, connectionName, skipIfIdentical }) => {
17
+ reuseConnection: z.boolean().optional().describe("Default true. Reuse the cached SSH connection for SFTP. Set false after a timeout or suspected stale cached SSH connection to force a fresh TCP/SSH connection for this transfer; the fresh connection closes afterwards."),
18
+ timeout: z.number().positive().optional().describe("Timeout in ms for SSH setup and SFTP channel opening. Transfer stream duration itself is not forcibly interrupted by this option."),
19
+ vvv: z.boolean().optional().describe("Default false. Append bounded SSH/SFTP debug output. For fresh ssh2 handshake logs, also set reuseConnection=false."),
20
+ fast: z.boolean().optional().describe("Default false. When true, use ssh2 fastPut for a single-file upload, which performs parallel SFTP reads/writes for better throughput. If a shell script needs CRLF-to-LF conversion, the upload falls back to the normal safe path."),
21
+ sftpConcurrency: z.number().int().positive().optional().describe("Only used when fast=true. Number of concurrent SFTP chunks for ssh2 fastPut; omitted uses ssh2's default."),
22
+ chunkSize: z.number().int().positive().optional().describe("Only used when fast=true. Chunk size in bytes for ssh2 fastPut; omitted uses ssh2's default."),
23
+ }, async ({ localPath, remotePath, connectionName, skipIfIdentical, reuseConnection, timeout, vvv, fast, sftpConcurrency, chunkSize }) => {
18
24
  try {
19
25
  const resolvedName = sshManager.resolveServer(connectionName);
20
26
  const result = await sshManager.upload(localPath, remotePath, resolvedName, {
21
27
  skipIfIdentical: skipIfIdentical !== false,
28
+ reuseConnection,
29
+ timeout,
30
+ vvv,
31
+ fast,
32
+ sftpConcurrency,
33
+ chunkSize,
22
34
  });
23
35
  return {
24
36
  content: [{ type: "text", text: result }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaarc/handfree-ssh-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",