@prmichaelsen/acp-mcp 0.7.0 → 1.0.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/dist/server.js CHANGED
@@ -433,70 +433,6 @@ function loadSSHPrivateKey() {
433
433
  }
434
434
  }
435
435
 
436
- // src/tools/acp-remote-list-files.ts
437
- var acpRemoteListFilesTool = {
438
- name: "acp_remote_list_files",
439
- description: "List files and directories in a specified path on the remote machine via SSH. Returns comprehensive metadata including permissions, timestamps, size, and ownership. Includes hidden files by default.",
440
- inputSchema: {
441
- type: "object",
442
- properties: {
443
- path: {
444
- type: "string",
445
- description: "The directory path to list files from"
446
- },
447
- recursive: {
448
- type: "boolean",
449
- description: "Whether to list files recursively",
450
- default: false
451
- },
452
- includeHidden: {
453
- type: "boolean",
454
- description: "Whether to include hidden files (starting with .)",
455
- default: true
456
- }
457
- },
458
- required: ["path"]
459
- }
460
- };
461
- async function handleAcpRemoteListFiles(args, sshConnection) {
462
- const { path, recursive = false, includeHidden = true } = args;
463
- try {
464
- const entries = await listRemoteFiles(sshConnection, path, recursive, includeHidden);
465
- const output = JSON.stringify(entries, null, 2);
466
- return {
467
- content: [
468
- {
469
- type: "text",
470
- text: output
471
- }
472
- ]
473
- };
474
- } catch (error) {
475
- const errorMessage = error instanceof Error ? error.message : String(error);
476
- return {
477
- content: [
478
- {
479
- type: "text",
480
- text: `Error listing remote files: ${errorMessage}`
481
- }
482
- ]
483
- };
484
- }
485
- }
486
- async function listRemoteFiles(ssh, dirPath, recursive, includeHidden) {
487
- const entries = await ssh.listFiles(dirPath, includeHidden);
488
- const allEntries = [...entries];
489
- if (recursive) {
490
- for (const entry of entries) {
491
- if (entry.type === "directory") {
492
- const subEntries = await listRemoteFiles(ssh, entry.path, recursive, includeHidden);
493
- allEntries.push(...subEntries);
494
- }
495
- }
496
- }
497
- return allEntries;
498
- }
499
-
500
436
  // src/utils/logger.ts
501
437
  var LOG_LEVELS = {
502
438
  error: 0,
@@ -746,195 +682,8 @@ async function executeWithProgress(command, cwd, sshConnection, progressToken, s
746
682
  };
747
683
  }
748
684
 
749
- // src/tools/acp-remote-read-file.ts
750
- var acpRemoteReadFileTool = {
751
- name: "acp_remote_read_file",
752
- description: "Read file contents from the remote machine via SSH",
753
- inputSchema: {
754
- type: "object",
755
- properties: {
756
- path: {
757
- type: "string",
758
- description: "Absolute path to file"
759
- },
760
- encoding: {
761
- type: "string",
762
- description: "File encoding (default: utf-8)",
763
- default: "utf-8",
764
- enum: ["utf-8", "ascii", "base64"]
765
- },
766
- maxSize: {
767
- type: "number",
768
- description: "Max file size in bytes (default: 1MB)",
769
- default: 1048576
770
- }
771
- },
772
- required: ["path"]
773
- }
774
- };
775
- async function handleAcpRemoteReadFile(args, sshConnection) {
776
- const { path, encoding = "utf-8", maxSize = 1048576 } = args;
777
- logger.debug("Reading remote file", { path, encoding, maxSize });
778
- try {
779
- const result = await sshConnection.readFile(path, encoding, maxSize);
780
- logger.debug("File read successful", { path, size: result.size });
781
- const output = {
782
- content: result.content,
783
- size: result.size,
784
- encoding: result.encoding
785
- };
786
- return {
787
- content: [
788
- {
789
- type: "text",
790
- text: JSON.stringify(output, null, 2)
791
- }
792
- ]
793
- };
794
- } catch (error) {
795
- const errorMessage = error instanceof Error ? error.message : String(error);
796
- logger.error("File read error", { path, error: errorMessage });
797
- return {
798
- content: [
799
- {
800
- type: "text",
801
- text: JSON.stringify({
802
- error: errorMessage,
803
- content: "",
804
- size: 0,
805
- encoding
806
- }, null, 2)
807
- }
808
- ]
809
- };
810
- }
811
- }
812
-
813
- // src/tools/acp-remote-write-file.ts
814
- var acpRemoteWriteFileTool = {
815
- name: "acp_remote_write_file",
816
- description: "Write file contents to the remote machine via SSH",
817
- inputSchema: {
818
- type: "object",
819
- properties: {
820
- path: {
821
- type: "string",
822
- description: "Absolute path to file"
823
- },
824
- content: {
825
- type: "string",
826
- description: "File contents to write"
827
- },
828
- encoding: {
829
- type: "string",
830
- description: "File encoding (default: utf-8)",
831
- default: "utf-8"
832
- },
833
- createDirs: {
834
- type: "boolean",
835
- description: "Create parent directories if they don't exist (default: false)",
836
- default: false
837
- },
838
- backup: {
839
- type: "boolean",
840
- description: "Backup existing file before overwriting (default: false)",
841
- default: false
842
- }
843
- },
844
- required: ["path", "content"]
845
- }
846
- };
847
- async function handleAcpRemoteWriteFile(args, sshConnection) {
848
- const { path, content, encoding = "utf-8", createDirs = false, backup = false } = args;
849
- logger.debug("Writing remote file", { path, contentSize: content.length, encoding, createDirs, backup });
850
- try {
851
- const result = await sshConnection.writeFile(path, content, {
852
- encoding,
853
- createDirs,
854
- backup
855
- });
856
- logger.debug("File write successful", { path, bytesWritten: result.bytesWritten, backupPath: result.backupPath });
857
- const output = {
858
- success: result.success,
859
- bytesWritten: result.bytesWritten,
860
- backupPath: result.backupPath
861
- };
862
- return {
863
- content: [
864
- {
865
- type: "text",
866
- text: JSON.stringify(output, null, 2)
867
- }
868
- ]
869
- };
870
- } catch (error) {
871
- const errorMessage = error instanceof Error ? error.message : String(error);
872
- logger.error("File write error", { path, error: errorMessage });
873
- return {
874
- content: [
875
- {
876
- type: "text",
877
- text: JSON.stringify({
878
- success: false,
879
- bytesWritten: 0,
880
- error: errorMessage
881
- }, null, 2)
882
- }
883
- ]
884
- };
885
- }
886
- }
887
-
888
685
  // src/utils/ssh-connection.ts
889
686
  import { Client } from "ssh2";
890
-
891
- // src/types/file-entry.ts
892
- function modeToPermissionString(mode) {
893
- const perms = [
894
- mode & 256 ? "r" : "-",
895
- mode & 128 ? "w" : "-",
896
- mode & 64 ? "x" : "-",
897
- mode & 32 ? "r" : "-",
898
- mode & 16 ? "w" : "-",
899
- mode & 8 ? "x" : "-",
900
- mode & 4 ? "r" : "-",
901
- mode & 2 ? "w" : "-",
902
- mode & 1 ? "x" : "-"
903
- ];
904
- return perms.join("");
905
- }
906
- function parsePermissions(mode) {
907
- return {
908
- mode,
909
- string: modeToPermissionString(mode),
910
- owner: {
911
- read: (mode & 256) !== 0,
912
- write: (mode & 128) !== 0,
913
- execute: (mode & 64) !== 0
914
- },
915
- group: {
916
- read: (mode & 32) !== 0,
917
- write: (mode & 16) !== 0,
918
- execute: (mode & 8) !== 0
919
- },
920
- others: {
921
- read: (mode & 4) !== 0,
922
- write: (mode & 2) !== 0,
923
- execute: (mode & 1) !== 0
924
- }
925
- };
926
- }
927
- function getFileType(stats) {
928
- if (stats.isDirectory())
929
- return "directory";
930
- if (stats.isFile())
931
- return "file";
932
- if (stats.isSymbolicLink())
933
- return "symlink";
934
- return "other";
935
- }
936
-
937
- // src/utils/ssh-connection.ts
938
687
  var SSHConnectionManager = class {
939
688
  client;
940
689
  config;
@@ -1015,9 +764,10 @@ var SSHConnectionManager = class {
1015
764
  await this.connect();
1016
765
  }
1017
766
  const startTime = Date.now();
1018
- logger.sshCommand(command, void 0, timeoutSeconds);
767
+ const wrappedCommand = this.wrapCommandWithShellInit(command);
768
+ logger.sshCommand(wrappedCommand, void 0, timeoutSeconds);
1019
769
  const execPromise = new Promise((resolve, reject) => {
1020
- this.client.exec(command, (err, stream) => {
770
+ this.client.exec(wrappedCommand, (err, stream) => {
1021
771
  if (err) {
1022
772
  reject(err);
1023
773
  return;
@@ -1073,10 +823,11 @@ var SSHConnectionManager = class {
1073
823
  await this.connect();
1074
824
  }
1075
825
  const fullCommand = cwd ? `cd "${cwd}" && ${command}` : command;
826
+ const wrappedCommand = this.wrapCommandWithShellInit(fullCommand);
1076
827
  const startTime = Date.now();
1077
- logger.sshCommand(fullCommand, cwd);
828
+ logger.sshCommand(wrappedCommand, cwd);
1078
829
  return new Promise((resolve, reject) => {
1079
- this.client.exec(fullCommand, (err, stream) => {
830
+ this.client.exec(wrappedCommand, (err, stream) => {
1080
831
  if (err) {
1081
832
  logger.error("SSH exec failed", {
1082
833
  command: fullCommand,
@@ -1112,241 +863,15 @@ var SSHConnectionManager = class {
1112
863
  });
1113
864
  }
1114
865
  /**
1115
- * Get SFTP wrapper for file operations
1116
- */
1117
- async getSFTP() {
1118
- if (!this.connected) {
1119
- await this.connect();
1120
- }
1121
- return new Promise((resolve, reject) => {
1122
- this.client.sftp((err, sftp) => {
1123
- if (err) {
1124
- reject(err);
1125
- } else {
1126
- resolve(sftp);
1127
- }
1128
- });
1129
- });
1130
- }
1131
- /**
1132
- * List files in a directory with comprehensive metadata
1133
- * Uses hybrid approach: shell ls for filenames (includes hidden), SFTP stat for metadata
866
+ * Wrap command to source shell configuration files
867
+ * This ensures PATH and other environment variables are properly set
868
+ * SSH non-interactive shells don't source ~/.bashrc or ~/.zshrc by default
1134
869
  *
1135
- * @param path - Directory path to list
1136
- * @param includeHidden - Whether to include hidden files (default: true)
1137
- * @returns Array of FileEntry objects with complete metadata
1138
- */
1139
- async listFiles(path, includeHidden = true) {
1140
- const startTime = Date.now();
1141
- logger.debug("Listing files", { path, includeHidden });
1142
- try {
1143
- const lsFlag = includeHidden ? "-A" : "";
1144
- const command = `ls ${lsFlag} -1 "${path}" 2>/dev/null`;
1145
- const result = await this.execWithTimeout(command, 10);
1146
- if (result.exitCode !== 0) {
1147
- throw new Error(`ls command failed: ${result.stderr}`);
1148
- }
1149
- const filenames = result.stdout.split("\n").map((f) => f.trim()).filter((f) => f !== "" && f !== "." && f !== "..");
1150
- logger.debug("Filenames retrieved via shell", {
1151
- path,
1152
- count: filenames.length,
1153
- method: "shell"
1154
- });
1155
- const sftp = await this.getSFTP();
1156
- const entries = [];
1157
- for (const filename of filenames) {
1158
- const fullPath = `${path}/${filename}`.replace(/\/+/g, "/");
1159
- try {
1160
- const stats = await new Promise((resolve, reject) => {
1161
- sftp.stat(fullPath, (err, stats2) => {
1162
- if (err)
1163
- reject(err);
1164
- else
1165
- resolve(stats2);
1166
- });
1167
- });
1168
- entries.push({
1169
- name: filename,
1170
- path: fullPath,
1171
- type: getFileType(stats),
1172
- size: stats.size,
1173
- permissions: parsePermissions(stats.mode),
1174
- owner: {
1175
- uid: stats.uid,
1176
- gid: stats.gid
1177
- },
1178
- timestamps: {
1179
- accessed: new Date(stats.atime * 1e3).toISOString(),
1180
- modified: new Date(stats.mtime * 1e3).toISOString()
1181
- }
1182
- });
1183
- } catch (error) {
1184
- logger.warn("Failed to stat file, skipping", {
1185
- path: fullPath,
1186
- error: error instanceof Error ? error.message : String(error)
1187
- });
1188
- }
1189
- }
1190
- const duration = Date.now() - startTime;
1191
- logger.debug("Files listed successfully", {
1192
- path,
1193
- count: entries.length,
1194
- duration: `${duration}ms`,
1195
- method: "hybrid"
1196
- });
1197
- return entries;
1198
- } catch (error) {
1199
- logger.warn("Shell ls command failed, falling back to SFTP readdir", {
1200
- path,
1201
- error: error instanceof Error ? error.message : String(error)
1202
- });
1203
- return this.listFilesViaSFTP(path, includeHidden);
1204
- }
1205
- }
1206
- /**
1207
- * Fallback method: List files using SFTP readdir (may miss hidden files)
1208
- * @private
1209
- */
1210
- async listFilesViaSFTP(path, includeHidden) {
1211
- const sftp = await this.getSFTP();
1212
- return new Promise((resolve, reject) => {
1213
- sftp.readdir(path, (err, list) => {
1214
- if (err) {
1215
- logger.error("SFTP readdir failed", { path, error: err.message });
1216
- reject(err);
1217
- return;
1218
- }
1219
- let entries = list.map((item) => ({
1220
- name: item.filename,
1221
- path: `${path}/${item.filename}`.replace(/\/+/g, "/"),
1222
- type: getFileType(item.attrs),
1223
- size: item.attrs.size,
1224
- permissions: parsePermissions(item.attrs.mode),
1225
- owner: {
1226
- uid: item.attrs.uid,
1227
- gid: item.attrs.gid
1228
- },
1229
- timestamps: {
1230
- accessed: new Date(item.attrs.atime * 1e3).toISOString(),
1231
- modified: new Date(item.attrs.mtime * 1e3).toISOString()
1232
- }
1233
- }));
1234
- if (!includeHidden) {
1235
- entries = entries.filter((e) => !e.name.startsWith("."));
1236
- }
1237
- logger.debug("Files listed via SFTP fallback", {
1238
- path,
1239
- count: entries.length,
1240
- method: "sftp",
1241
- note: "Hidden files may be missing (SFTP limitation)"
1242
- });
1243
- resolve(entries);
1244
- });
1245
- });
1246
- }
1247
- /**
1248
- * Read file contents from remote machine
870
+ * @param command - The command to wrap
871
+ * @returns Wrapped command that sources shell config first
1249
872
  */
1250
- async readFile(path, encoding = "utf-8", maxSize = 1048576) {
1251
- const startTime = Date.now();
1252
- logger.fileOperation("read", path, { encoding, maxSize });
1253
- const sftp = await this.getSFTP();
1254
- return new Promise((resolve, reject) => {
1255
- sftp.stat(path, (err, stats) => {
1256
- if (err) {
1257
- logger.error("File stat failed", { path, error: err.message });
1258
- reject(new Error(`File not found or inaccessible: ${path}`));
1259
- return;
1260
- }
1261
- logger.debug("File stat retrieved", { path, size: stats.size });
1262
- if (stats.size > maxSize) {
1263
- logger.warn("File too large", { path, size: stats.size, maxSize });
1264
- reject(new Error(`File too large: ${stats.size} bytes (max: ${maxSize} bytes)`));
1265
- return;
1266
- }
1267
- sftp.readFile(path, { encoding }, (err2, data) => {
1268
- if (err2) {
1269
- logger.error("File read failed", { path, error: err2.message });
1270
- reject(new Error(`Failed to read file: ${err2.message}`));
1271
- return;
1272
- }
1273
- const duration = Date.now() - startTime;
1274
- logger.debug("File read completed", { path, size: stats.size, duration: `${duration}ms` });
1275
- resolve({
1276
- content: data.toString(),
1277
- size: stats.size,
1278
- encoding
1279
- });
1280
- });
1281
- });
1282
- });
1283
- }
1284
- /**
1285
- * Write file contents to remote machine
1286
- */
1287
- async writeFile(path, content, options = {}) {
1288
- const { encoding = "utf-8", createDirs = false, backup = false } = options;
1289
- const startTime = Date.now();
1290
- logger.fileOperation("write", path, {
1291
- contentSize: content.length,
1292
- encoding,
1293
- createDirs,
1294
- backup
1295
- });
1296
- const sftp = await this.getSFTP();
1297
- return new Promise((resolve, reject) => {
1298
- const writeOperation = () => {
1299
- if (backup) {
1300
- const backupPath = `${path}.backup`;
1301
- sftp.rename(path, backupPath, (err) => {
1302
- if (err && err.message !== "No such file") {
1303
- reject(new Error(`Failed to create backup: ${err.message}`));
1304
- return;
1305
- }
1306
- performWrite(backupPath);
1307
- });
1308
- } else {
1309
- performWrite();
1310
- }
1311
- };
1312
- const performWrite = (backupPath) => {
1313
- const buffer = Buffer.from(content, encoding);
1314
- const tempPath = `${path}.tmp`;
1315
- sftp.writeFile(tempPath, buffer, (err) => {
1316
- if (err) {
1317
- reject(new Error(`Failed to write file: ${err.message}`));
1318
- return;
1319
- }
1320
- sftp.rename(tempPath, path, (err2) => {
1321
- if (err2) {
1322
- logger.error("File rename failed", { tempPath, path, error: err2.message });
1323
- reject(new Error(`Failed to rename temp file: ${err2.message}`));
1324
- return;
1325
- }
1326
- const duration = Date.now() - startTime;
1327
- logger.debug("File write completed", {
1328
- path,
1329
- bytesWritten: buffer.length,
1330
- duration: `${duration}ms`,
1331
- backupPath
1332
- });
1333
- resolve({
1334
- success: true,
1335
- bytesWritten: buffer.length,
1336
- backupPath
1337
- });
1338
- });
1339
- });
1340
- };
1341
- if (createDirs) {
1342
- const dirPath = path.substring(0, path.lastIndexOf("/"));
1343
- this.exec(`mkdir -p ${dirPath}`).then(() => {
1344
- writeOperation();
1345
- }).catch(reject);
1346
- } else {
1347
- writeOperation();
1348
- }
1349
- });
873
+ wrapCommandWithShellInit(command) {
874
+ return `(source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true) && ${command}`;
1350
875
  }
1351
876
  /**
1352
877
  * Disconnect from the SSH server
@@ -1393,7 +918,7 @@ async function main() {
1393
918
  );
1394
919
  server.setRequestHandler(ListToolsRequestSchema, async () => {
1395
920
  logger.debug("Tool discovery requested");
1396
- const tools = [acpRemoteListFilesTool, acpRemoteExecuteCommandTool, acpRemoteReadFileTool, acpRemoteWriteFileTool];
921
+ const tools = [acpRemoteExecuteCommandTool];
1397
922
  logger.debug(`Returning ${tools.length} tools`, { tools: tools.map((t) => t.name) });
1398
923
  return { tools };
1399
924
  });
@@ -1402,14 +927,8 @@ async function main() {
1402
927
  logger.toolInvoked(request.params.name, request.params.arguments);
1403
928
  try {
1404
929
  let result;
1405
- if (request.params.name === "acp_remote_list_files") {
1406
- result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
1407
- } else if (request.params.name === "acp_remote_execute_command") {
930
+ if (request.params.name === "acp_remote_execute_command") {
1408
931
  result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection, extra, server);
1409
- } else if (request.params.name === "acp_remote_read_file") {
1410
- result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
1411
- } else if (request.params.name === "acp_remote_write_file") {
1412
- result = await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
1413
932
  } else {
1414
933
  throw new Error(`Unknown tool: ${request.params.name}`);
1415
934
  }