@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.
@@ -5,70 +5,6 @@ import {
5
5
  ListToolsRequestSchema
6
6
  } from "@modelcontextprotocol/sdk/types.js";
7
7
 
8
- // src/tools/acp-remote-list-files.ts
9
- var acpRemoteListFilesTool = {
10
- name: "acp_remote_list_files",
11
- 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.",
12
- inputSchema: {
13
- type: "object",
14
- properties: {
15
- path: {
16
- type: "string",
17
- description: "The directory path to list files from"
18
- },
19
- recursive: {
20
- type: "boolean",
21
- description: "Whether to list files recursively",
22
- default: false
23
- },
24
- includeHidden: {
25
- type: "boolean",
26
- description: "Whether to include hidden files (starting with .)",
27
- default: true
28
- }
29
- },
30
- required: ["path"]
31
- }
32
- };
33
- async function handleAcpRemoteListFiles(args, sshConnection) {
34
- const { path, recursive = false, includeHidden = true } = args;
35
- try {
36
- const entries = await listRemoteFiles(sshConnection, path, recursive, includeHidden);
37
- const output = JSON.stringify(entries, null, 2);
38
- return {
39
- content: [
40
- {
41
- type: "text",
42
- text: output
43
- }
44
- ]
45
- };
46
- } catch (error) {
47
- const errorMessage = error instanceof Error ? error.message : String(error);
48
- return {
49
- content: [
50
- {
51
- type: "text",
52
- text: `Error listing remote files: ${errorMessage}`
53
- }
54
- ]
55
- };
56
- }
57
- }
58
- async function listRemoteFiles(ssh, dirPath, recursive, includeHidden) {
59
- const entries = await ssh.listFiles(dirPath, includeHidden);
60
- const allEntries = [...entries];
61
- if (recursive) {
62
- for (const entry of entries) {
63
- if (entry.type === "directory") {
64
- const subEntries = await listRemoteFiles(ssh, entry.path, recursive, includeHidden);
65
- allEntries.push(...subEntries);
66
- }
67
- }
68
- }
69
- return allEntries;
70
- }
71
-
72
8
  // src/utils/logger.ts
73
9
  var LOG_LEVELS = {
74
10
  error: 0,
@@ -318,195 +254,8 @@ async function executeWithProgress(command, cwd, sshConnection, progressToken, s
318
254
  };
319
255
  }
320
256
 
321
- // src/tools/acp-remote-read-file.ts
322
- var acpRemoteReadFileTool = {
323
- name: "acp_remote_read_file",
324
- description: "Read file contents from the remote machine via SSH",
325
- inputSchema: {
326
- type: "object",
327
- properties: {
328
- path: {
329
- type: "string",
330
- description: "Absolute path to file"
331
- },
332
- encoding: {
333
- type: "string",
334
- description: "File encoding (default: utf-8)",
335
- default: "utf-8",
336
- enum: ["utf-8", "ascii", "base64"]
337
- },
338
- maxSize: {
339
- type: "number",
340
- description: "Max file size in bytes (default: 1MB)",
341
- default: 1048576
342
- }
343
- },
344
- required: ["path"]
345
- }
346
- };
347
- async function handleAcpRemoteReadFile(args, sshConnection) {
348
- const { path, encoding = "utf-8", maxSize = 1048576 } = args;
349
- logger.debug("Reading remote file", { path, encoding, maxSize });
350
- try {
351
- const result = await sshConnection.readFile(path, encoding, maxSize);
352
- logger.debug("File read successful", { path, size: result.size });
353
- const output = {
354
- content: result.content,
355
- size: result.size,
356
- encoding: result.encoding
357
- };
358
- return {
359
- content: [
360
- {
361
- type: "text",
362
- text: JSON.stringify(output, null, 2)
363
- }
364
- ]
365
- };
366
- } catch (error) {
367
- const errorMessage = error instanceof Error ? error.message : String(error);
368
- logger.error("File read error", { path, error: errorMessage });
369
- return {
370
- content: [
371
- {
372
- type: "text",
373
- text: JSON.stringify({
374
- error: errorMessage,
375
- content: "",
376
- size: 0,
377
- encoding
378
- }, null, 2)
379
- }
380
- ]
381
- };
382
- }
383
- }
384
-
385
- // src/tools/acp-remote-write-file.ts
386
- var acpRemoteWriteFileTool = {
387
- name: "acp_remote_write_file",
388
- description: "Write file contents to the remote machine via SSH",
389
- inputSchema: {
390
- type: "object",
391
- properties: {
392
- path: {
393
- type: "string",
394
- description: "Absolute path to file"
395
- },
396
- content: {
397
- type: "string",
398
- description: "File contents to write"
399
- },
400
- encoding: {
401
- type: "string",
402
- description: "File encoding (default: utf-8)",
403
- default: "utf-8"
404
- },
405
- createDirs: {
406
- type: "boolean",
407
- description: "Create parent directories if they don't exist (default: false)",
408
- default: false
409
- },
410
- backup: {
411
- type: "boolean",
412
- description: "Backup existing file before overwriting (default: false)",
413
- default: false
414
- }
415
- },
416
- required: ["path", "content"]
417
- }
418
- };
419
- async function handleAcpRemoteWriteFile(args, sshConnection) {
420
- const { path, content, encoding = "utf-8", createDirs = false, backup = false } = args;
421
- logger.debug("Writing remote file", { path, contentSize: content.length, encoding, createDirs, backup });
422
- try {
423
- const result = await sshConnection.writeFile(path, content, {
424
- encoding,
425
- createDirs,
426
- backup
427
- });
428
- logger.debug("File write successful", { path, bytesWritten: result.bytesWritten, backupPath: result.backupPath });
429
- const output = {
430
- success: result.success,
431
- bytesWritten: result.bytesWritten,
432
- backupPath: result.backupPath
433
- };
434
- return {
435
- content: [
436
- {
437
- type: "text",
438
- text: JSON.stringify(output, null, 2)
439
- }
440
- ]
441
- };
442
- } catch (error) {
443
- const errorMessage = error instanceof Error ? error.message : String(error);
444
- logger.error("File write error", { path, error: errorMessage });
445
- return {
446
- content: [
447
- {
448
- type: "text",
449
- text: JSON.stringify({
450
- success: false,
451
- bytesWritten: 0,
452
- error: errorMessage
453
- }, null, 2)
454
- }
455
- ]
456
- };
457
- }
458
- }
459
-
460
257
  // src/utils/ssh-connection.ts
461
258
  import { Client } from "ssh2";
462
-
463
- // src/types/file-entry.ts
464
- function modeToPermissionString(mode) {
465
- const perms = [
466
- mode & 256 ? "r" : "-",
467
- mode & 128 ? "w" : "-",
468
- mode & 64 ? "x" : "-",
469
- mode & 32 ? "r" : "-",
470
- mode & 16 ? "w" : "-",
471
- mode & 8 ? "x" : "-",
472
- mode & 4 ? "r" : "-",
473
- mode & 2 ? "w" : "-",
474
- mode & 1 ? "x" : "-"
475
- ];
476
- return perms.join("");
477
- }
478
- function parsePermissions(mode) {
479
- return {
480
- mode,
481
- string: modeToPermissionString(mode),
482
- owner: {
483
- read: (mode & 256) !== 0,
484
- write: (mode & 128) !== 0,
485
- execute: (mode & 64) !== 0
486
- },
487
- group: {
488
- read: (mode & 32) !== 0,
489
- write: (mode & 16) !== 0,
490
- execute: (mode & 8) !== 0
491
- },
492
- others: {
493
- read: (mode & 4) !== 0,
494
- write: (mode & 2) !== 0,
495
- execute: (mode & 1) !== 0
496
- }
497
- };
498
- }
499
- function getFileType(stats) {
500
- if (stats.isDirectory())
501
- return "directory";
502
- if (stats.isFile())
503
- return "file";
504
- if (stats.isSymbolicLink())
505
- return "symlink";
506
- return "other";
507
- }
508
-
509
- // src/utils/ssh-connection.ts
510
259
  var SSHConnectionManager = class {
511
260
  client;
512
261
  config;
@@ -587,9 +336,10 @@ var SSHConnectionManager = class {
587
336
  await this.connect();
588
337
  }
589
338
  const startTime = Date.now();
590
- logger.sshCommand(command, void 0, timeoutSeconds);
339
+ const wrappedCommand = this.wrapCommandWithShellInit(command);
340
+ logger.sshCommand(wrappedCommand, void 0, timeoutSeconds);
591
341
  const execPromise = new Promise((resolve, reject) => {
592
- this.client.exec(command, (err, stream) => {
342
+ this.client.exec(wrappedCommand, (err, stream) => {
593
343
  if (err) {
594
344
  reject(err);
595
345
  return;
@@ -645,10 +395,11 @@ var SSHConnectionManager = class {
645
395
  await this.connect();
646
396
  }
647
397
  const fullCommand = cwd ? `cd "${cwd}" && ${command}` : command;
398
+ const wrappedCommand = this.wrapCommandWithShellInit(fullCommand);
648
399
  const startTime = Date.now();
649
- logger.sshCommand(fullCommand, cwd);
400
+ logger.sshCommand(wrappedCommand, cwd);
650
401
  return new Promise((resolve, reject) => {
651
- this.client.exec(fullCommand, (err, stream) => {
402
+ this.client.exec(wrappedCommand, (err, stream) => {
652
403
  if (err) {
653
404
  logger.error("SSH exec failed", {
654
405
  command: fullCommand,
@@ -684,241 +435,15 @@ var SSHConnectionManager = class {
684
435
  });
685
436
  }
686
437
  /**
687
- * Get SFTP wrapper for file operations
688
- */
689
- async getSFTP() {
690
- if (!this.connected) {
691
- await this.connect();
692
- }
693
- return new Promise((resolve, reject) => {
694
- this.client.sftp((err, sftp) => {
695
- if (err) {
696
- reject(err);
697
- } else {
698
- resolve(sftp);
699
- }
700
- });
701
- });
702
- }
703
- /**
704
- * List files in a directory with comprehensive metadata
705
- * Uses hybrid approach: shell ls for filenames (includes hidden), SFTP stat for metadata
438
+ * Wrap command to source shell configuration files
439
+ * This ensures PATH and other environment variables are properly set
440
+ * SSH non-interactive shells don't source ~/.bashrc or ~/.zshrc by default
706
441
  *
707
- * @param path - Directory path to list
708
- * @param includeHidden - Whether to include hidden files (default: true)
709
- * @returns Array of FileEntry objects with complete metadata
710
- */
711
- async listFiles(path, includeHidden = true) {
712
- const startTime = Date.now();
713
- logger.debug("Listing files", { path, includeHidden });
714
- try {
715
- const lsFlag = includeHidden ? "-A" : "";
716
- const command = `ls ${lsFlag} -1 "${path}" 2>/dev/null`;
717
- const result = await this.execWithTimeout(command, 10);
718
- if (result.exitCode !== 0) {
719
- throw new Error(`ls command failed: ${result.stderr}`);
720
- }
721
- const filenames = result.stdout.split("\n").map((f) => f.trim()).filter((f) => f !== "" && f !== "." && f !== "..");
722
- logger.debug("Filenames retrieved via shell", {
723
- path,
724
- count: filenames.length,
725
- method: "shell"
726
- });
727
- const sftp = await this.getSFTP();
728
- const entries = [];
729
- for (const filename of filenames) {
730
- const fullPath = `${path}/${filename}`.replace(/\/+/g, "/");
731
- try {
732
- const stats = await new Promise((resolve, reject) => {
733
- sftp.stat(fullPath, (err, stats2) => {
734
- if (err)
735
- reject(err);
736
- else
737
- resolve(stats2);
738
- });
739
- });
740
- entries.push({
741
- name: filename,
742
- path: fullPath,
743
- type: getFileType(stats),
744
- size: stats.size,
745
- permissions: parsePermissions(stats.mode),
746
- owner: {
747
- uid: stats.uid,
748
- gid: stats.gid
749
- },
750
- timestamps: {
751
- accessed: new Date(stats.atime * 1e3).toISOString(),
752
- modified: new Date(stats.mtime * 1e3).toISOString()
753
- }
754
- });
755
- } catch (error) {
756
- logger.warn("Failed to stat file, skipping", {
757
- path: fullPath,
758
- error: error instanceof Error ? error.message : String(error)
759
- });
760
- }
761
- }
762
- const duration = Date.now() - startTime;
763
- logger.debug("Files listed successfully", {
764
- path,
765
- count: entries.length,
766
- duration: `${duration}ms`,
767
- method: "hybrid"
768
- });
769
- return entries;
770
- } catch (error) {
771
- logger.warn("Shell ls command failed, falling back to SFTP readdir", {
772
- path,
773
- error: error instanceof Error ? error.message : String(error)
774
- });
775
- return this.listFilesViaSFTP(path, includeHidden);
776
- }
777
- }
778
- /**
779
- * Fallback method: List files using SFTP readdir (may miss hidden files)
780
- * @private
442
+ * @param command - The command to wrap
443
+ * @returns Wrapped command that sources shell config first
781
444
  */
782
- async listFilesViaSFTP(path, includeHidden) {
783
- const sftp = await this.getSFTP();
784
- return new Promise((resolve, reject) => {
785
- sftp.readdir(path, (err, list) => {
786
- if (err) {
787
- logger.error("SFTP readdir failed", { path, error: err.message });
788
- reject(err);
789
- return;
790
- }
791
- let entries = list.map((item) => ({
792
- name: item.filename,
793
- path: `${path}/${item.filename}`.replace(/\/+/g, "/"),
794
- type: getFileType(item.attrs),
795
- size: item.attrs.size,
796
- permissions: parsePermissions(item.attrs.mode),
797
- owner: {
798
- uid: item.attrs.uid,
799
- gid: item.attrs.gid
800
- },
801
- timestamps: {
802
- accessed: new Date(item.attrs.atime * 1e3).toISOString(),
803
- modified: new Date(item.attrs.mtime * 1e3).toISOString()
804
- }
805
- }));
806
- if (!includeHidden) {
807
- entries = entries.filter((e) => !e.name.startsWith("."));
808
- }
809
- logger.debug("Files listed via SFTP fallback", {
810
- path,
811
- count: entries.length,
812
- method: "sftp",
813
- note: "Hidden files may be missing (SFTP limitation)"
814
- });
815
- resolve(entries);
816
- });
817
- });
818
- }
819
- /**
820
- * Read file contents from remote machine
821
- */
822
- async readFile(path, encoding = "utf-8", maxSize = 1048576) {
823
- const startTime = Date.now();
824
- logger.fileOperation("read", path, { encoding, maxSize });
825
- const sftp = await this.getSFTP();
826
- return new Promise((resolve, reject) => {
827
- sftp.stat(path, (err, stats) => {
828
- if (err) {
829
- logger.error("File stat failed", { path, error: err.message });
830
- reject(new Error(`File not found or inaccessible: ${path}`));
831
- return;
832
- }
833
- logger.debug("File stat retrieved", { path, size: stats.size });
834
- if (stats.size > maxSize) {
835
- logger.warn("File too large", { path, size: stats.size, maxSize });
836
- reject(new Error(`File too large: ${stats.size} bytes (max: ${maxSize} bytes)`));
837
- return;
838
- }
839
- sftp.readFile(path, { encoding }, (err2, data) => {
840
- if (err2) {
841
- logger.error("File read failed", { path, error: err2.message });
842
- reject(new Error(`Failed to read file: ${err2.message}`));
843
- return;
844
- }
845
- const duration = Date.now() - startTime;
846
- logger.debug("File read completed", { path, size: stats.size, duration: `${duration}ms` });
847
- resolve({
848
- content: data.toString(),
849
- size: stats.size,
850
- encoding
851
- });
852
- });
853
- });
854
- });
855
- }
856
- /**
857
- * Write file contents to remote machine
858
- */
859
- async writeFile(path, content, options = {}) {
860
- const { encoding = "utf-8", createDirs = false, backup = false } = options;
861
- const startTime = Date.now();
862
- logger.fileOperation("write", path, {
863
- contentSize: content.length,
864
- encoding,
865
- createDirs,
866
- backup
867
- });
868
- const sftp = await this.getSFTP();
869
- return new Promise((resolve, reject) => {
870
- const writeOperation = () => {
871
- if (backup) {
872
- const backupPath = `${path}.backup`;
873
- sftp.rename(path, backupPath, (err) => {
874
- if (err && err.message !== "No such file") {
875
- reject(new Error(`Failed to create backup: ${err.message}`));
876
- return;
877
- }
878
- performWrite(backupPath);
879
- });
880
- } else {
881
- performWrite();
882
- }
883
- };
884
- const performWrite = (backupPath) => {
885
- const buffer = Buffer.from(content, encoding);
886
- const tempPath = `${path}.tmp`;
887
- sftp.writeFile(tempPath, buffer, (err) => {
888
- if (err) {
889
- reject(new Error(`Failed to write file: ${err.message}`));
890
- return;
891
- }
892
- sftp.rename(tempPath, path, (err2) => {
893
- if (err2) {
894
- logger.error("File rename failed", { tempPath, path, error: err2.message });
895
- reject(new Error(`Failed to rename temp file: ${err2.message}`));
896
- return;
897
- }
898
- const duration = Date.now() - startTime;
899
- logger.debug("File write completed", {
900
- path,
901
- bytesWritten: buffer.length,
902
- duration: `${duration}ms`,
903
- backupPath
904
- });
905
- resolve({
906
- success: true,
907
- bytesWritten: buffer.length,
908
- backupPath
909
- });
910
- });
911
- });
912
- };
913
- if (createDirs) {
914
- const dirPath = path.substring(0, path.lastIndexOf("/"));
915
- this.exec(`mkdir -p ${dirPath}`).then(() => {
916
- writeOperation();
917
- }).catch(reject);
918
- } else {
919
- writeOperation();
920
- }
921
- });
445
+ wrapCommandWithShellInit(command) {
446
+ return `(source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null || source ~/.profile 2>/dev/null || true) && ${command}`;
922
447
  }
923
448
  /**
924
449
  * Disconnect from the SSH server
@@ -965,7 +490,7 @@ async function createServer(serverConfig) {
965
490
  );
966
491
  server.setRequestHandler(ListToolsRequestSchema, async () => {
967
492
  logger.debug("Tool discovery requested", { userId: serverConfig.userId });
968
- const tools = [acpRemoteListFilesTool, acpRemoteExecuteCommandTool, acpRemoteReadFileTool, acpRemoteWriteFileTool];
493
+ const tools = [acpRemoteExecuteCommandTool];
969
494
  logger.debug(`Returning ${tools.length} tools`, { tools: tools.map((t) => t.name), userId: serverConfig.userId });
970
495
  return { tools };
971
496
  });
@@ -974,14 +499,8 @@ async function createServer(serverConfig) {
974
499
  logger.toolInvoked(request.params.name, request.params.arguments, serverConfig.userId);
975
500
  try {
976
501
  let result;
977
- if (request.params.name === "acp_remote_list_files") {
978
- result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
979
- } else if (request.params.name === "acp_remote_execute_command") {
502
+ if (request.params.name === "acp_remote_execute_command") {
980
503
  result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection, extra, server);
981
- } else if (request.params.name === "acp_remote_read_file") {
982
- result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
983
- } else if (request.params.name === "acp_remote_write_file") {
984
- result = await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
985
504
  } else {
986
505
  throw new Error(`Unknown tool: ${request.params.name}`);
987
506
  }