Skip to content

Commit 82ed7d2

Browse files
fix: macOS compatibility, config safety, and file CRLF preservation improvements
1 parent 9ce70f5 commit 82ed7d2

2 files changed

Lines changed: 55 additions & 13 deletions

File tree

plugins/remote-ssh/scripts/remote-ssh-mcp.js

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function loadHostConfig() {
7979
const configFile = process.env.REMOTE_SSH_CONFIG_FILE;
8080
if (configFile) {
8181
const resolved = expandHome(configFile);
82+
if (!fs.existsSync(resolved)) return {};
8283
const parsed = parseJson(fs.readFileSync(resolved, "utf8"), `REMOTE_SSH_CONFIG_FILE ${resolved}`);
8384
return parsed.hosts || parsed;
8485
}
@@ -123,9 +124,18 @@ function parseSshHost(input) {
123124
function cleanHostProfile(args) {
124125
const parsed = parseSshHost(args.sshHost);
125126
const allowedPaths = Array.isArray(args.allowedPaths) ? args.allowedPaths : [];
127+
128+
let port = 22;
129+
if (args.port) {
130+
const p = Number(args.port);
131+
if (Number.isInteger(p) && p > 0 && p <= 65535) {
132+
port = p;
133+
}
134+
}
135+
126136
const profile = {
127137
...parsed,
128-
port: args.port || 22,
138+
port,
129139
identityFile: args.identityFile || undefined,
130140
workspaceRoot: args.workspaceRoot || undefined,
131141
allowedPaths: allowedPaths.length > 0 ? allowedPaths : args.workspaceRoot ? [args.workspaceRoot] : [],
@@ -404,13 +414,15 @@ function buildBrowseDirCommand(remotePath, limit) {
404414
return [
405415
`test -d ${quotedPath}`,
406416
`cd ${quotedPath}`,
407-
[
408-
"find . -maxdepth 1 -mindepth 1 -type d",
409-
"-printf '%f\\t%p\\t%m\\t%u\\t%g\\t%TY-%Tm-%Td %TH:%TM\\n'",
410-
"| sort",
411-
`| head -n ${safeLimit}`,
412-
"| awk -F '\\t' -v root=\"$PWD\" 'BEGIN { OFS=\"\\t\" } { if ($2 == \"./\" $1) $2=root \"/\" $1; print }'",
413-
].join(" "),
417+
`if [ "$(uname)" = "Darwin" ]; then`,
418+
` find . -maxdepth 1 -mindepth 1 -type d | head -n ${safeLimit} | while read -r d; do`,
419+
` name="\${d#./}"`,
420+
` abs="${remotePath === "/" ? "" : remotePath}/\${name}"`,
421+
` stat -f "\${name}\\t\${abs}\\t%Lp\\t%Su\\t%Sg\\t%Sm" -t "%Y-%m-%d %H:%M" "\$d" 2>/dev/null`,
422+
` done`,
423+
`else`,
424+
` find . -maxdepth 1 -mindepth 1 -type d -printf '%f\\t%p\\t%m\\t%u\\t%g\\t%TY-%Tm-%Td %TH:%TM\\n' | sort | head -n ${safeLimit} | awk -F '\\t' -v root="$PWD" 'BEGIN { OFS="\\t" } { if ($2 == "./" $1) $2=root "/" $1; print }'`,
425+
`fi`,
414426
].join(" && ");
415427
}
416428

@@ -1065,12 +1077,12 @@ async function callTool(name, args) {
10651077
`p = pathlib.Path(${JSON.stringify(remotePath)})`,
10661078
`old = base64.b64decode(${JSON.stringify(base64Text(args.oldText))}).decode('utf-8')`,
10671079
`new = base64.b64decode(${JSON.stringify(base64Text(args.newText))}).decode('utf-8')`,
1068-
"text = p.read_text(encoding='utf-8')",
1080+
"text = p.read_bytes().decode('utf-8')",
10691081
"count = text.count(old)",
10701082
`expected = ${expected}`,
10711083
"if count != expected:",
10721084
" raise SystemExit(f'expected {expected} replacement(s), found {count}')",
1073-
"p.write_text(text.replace(old, new), encoding='utf-8')",
1085+
"p.write_bytes(text.replace(old, new).encode('utf-8'))",
10741086
"print(f'replaced {count} occurrence(s)')",
10751087
].join("\n");
10761088
const command = `python3 -c ${shellQuote(script)}`;
@@ -1103,9 +1115,19 @@ async function callTool(name, args) {
11031115
const remotePath = assertPathAllowed(config, args.path, "write");
11041116
const encoded = Buffer.from(args.content, "utf8").toString("base64");
11051117
const quotedPath = shellQuote(remotePath);
1106-
const writeCommand = args.overwrite
1107-
? `printf %s ${shellQuote(encoded)} | base64 -d > ${quotedPath}`
1108-
: `set -C; printf %s ${shellQuote(encoded)} | base64 -d > ${quotedPath}`;
1118+
const writeCommand = [
1119+
`decode_base64() {`,
1120+
` if base64 --decode < /dev/null >/dev/null 2>&1; then base64 --decode;`,
1121+
` elif base64 -d < /dev/null >/dev/null 2>&1; then base64 -d;`,
1122+
` elif base64 -D < /dev/null >/dev/null 2>&1; then base64 -D;`,
1123+
` elif openssl base64 -d < /dev/null >/dev/null 2>&1; then openssl base64 -d;`,
1124+
` else echo "No base64 decoder" >&2; exit 1; fi`,
1125+
`}`,
1126+
`mkdir -p "$(dirname ${quotedPath})"`,
1127+
args.overwrite
1128+
? `printf %s ${shellQuote(encoded)} | decode_base64 > ${quotedPath}`
1129+
: `set -C; printf %s ${shellQuote(encoded)} | decode_base64 > ${quotedPath}`,
1130+
].join("\n");
11091131
return textResult(await runSsh(config, writeCommand, name));
11101132
}
11111133

plugins/remote-ssh/test/remote-ssh-mcp.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,28 @@ assert.deepEqual(
9494

9595
assert.equal(base64Text("hello"), "aGVsbG8=");
9696

97+
assert.deepEqual(
98+
cleanHostProfile({
99+
sshHost: "test@example.com",
100+
port: "invalid",
101+
}),
102+
{
103+
user: "test",
104+
host: "example.com",
105+
port: 22,
106+
allowedPaths: [],
107+
allowWrites: false,
108+
strictHostKeyChecking: true,
109+
connectTimeoutSeconds: 15,
110+
commandTimeoutMs: 120000,
111+
maxOutputBytes: 1048576,
112+
}
113+
);
114+
97115
assert.match(buildBrowseDirCommand("/home/alice/My Project", 25), /find \. -maxdepth 1 -mindepth 1 -type d/);
98116
assert.match(buildBrowseDirCommand("/home/alice/My Project", 25), /head -n 25/);
117+
assert.match(buildBrowseDirCommand("/home/alice/My Project", 25), /Darwin/);
118+
assert.match(buildBrowseDirCommand("/home/alice/My Project", 25), /stat -f/);
99119

100120
assert.deepEqual(
101121
parseRemoteDirectoryEntries(

0 commit comments

Comments
 (0)