@prmichaelsen/acp-mcp 0.4.1 → 0.5.1
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/CHANGELOG.md +31 -0
- package/README.md +2 -0
- package/agent/milestones/milestone-2-bug-fixes-production-readiness.md +159 -0
- package/agent/progress.yaml +92 -13
- package/agent/tasks/task-4-fix-read-file-not-found-bug.md +390 -0
- package/dist/server-factory.js +212 -19
- package/dist/server-factory.js.map +3 -3
- package/dist/server.js +206 -22
- package/dist/server.js.map +3 -3
- package/dist/utils/logger.d.ts +43 -0
- package/package.json +1 -1
- package/src/server-factory.ts +45 -16
- package/src/server.ts +36 -18
- package/src/tools/acp-remote-execute-command.ts +11 -0
- package/src/tools/acp-remote-list-files.ts +7 -4
- package/src/tools/acp-remote-read-file.ts +6 -0
- package/src/tools/acp-remote-write-file.ts +6 -0
- package/src/utils/logger.ts +131 -0
- package/src/utils/ssh-connection.ts +59 -0
package/dist/server.js
CHANGED
|
@@ -481,20 +481,127 @@ async function listRemoteFiles(ssh, dirPath, recursive) {
|
|
|
481
481
|
const entries = await ssh.listFiles(dirPath);
|
|
482
482
|
const files = [];
|
|
483
483
|
for (const entry of entries) {
|
|
484
|
+
const fullPath = `${dirPath}/${entry.name}`.replace(/\/+/g, "/");
|
|
484
485
|
if (entry.isDirectory) {
|
|
485
|
-
files.push(`${
|
|
486
|
+
files.push(`${fullPath}/`);
|
|
486
487
|
if (recursive) {
|
|
487
|
-
const fullPath = `${dirPath}/${entry.name}`.replace(/\/+/g, "/");
|
|
488
488
|
const subFiles = await listRemoteFiles(ssh, fullPath, recursive);
|
|
489
|
-
files.push(...subFiles
|
|
489
|
+
files.push(...subFiles);
|
|
490
490
|
}
|
|
491
491
|
} else {
|
|
492
|
-
files.push(
|
|
492
|
+
files.push(fullPath);
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
return files.sort();
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
+
// src/utils/logger.ts
|
|
499
|
+
var LOG_LEVELS = {
|
|
500
|
+
error: 0,
|
|
501
|
+
warn: 1,
|
|
502
|
+
info: 2,
|
|
503
|
+
debug: 3,
|
|
504
|
+
trace: 4
|
|
505
|
+
};
|
|
506
|
+
var Logger = class {
|
|
507
|
+
level;
|
|
508
|
+
enabled;
|
|
509
|
+
constructor() {
|
|
510
|
+
this.level = process.env.ACP_MCP_LOG_LEVEL || "info";
|
|
511
|
+
this.enabled = process.env.ACP_MCP_DEBUG === "true" || process.env.NODE_ENV === "development";
|
|
512
|
+
}
|
|
513
|
+
shouldLog(level) {
|
|
514
|
+
if (!this.enabled && level !== "error" && level !== "warn") {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return LOG_LEVELS[level] <= LOG_LEVELS[this.level];
|
|
518
|
+
}
|
|
519
|
+
formatMessage(level, message, data) {
|
|
520
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
521
|
+
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
522
|
+
if (data !== void 0) {
|
|
523
|
+
const dataStr = typeof data === "object" ? JSON.stringify(data, null, 2) : String(data);
|
|
524
|
+
return `${prefix} ${message}
|
|
525
|
+
${dataStr}`;
|
|
526
|
+
}
|
|
527
|
+
return `${prefix} ${message}`;
|
|
528
|
+
}
|
|
529
|
+
error(message, data) {
|
|
530
|
+
if (this.shouldLog("error")) {
|
|
531
|
+
console.error(this.formatMessage("error", message, data));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
warn(message, data) {
|
|
535
|
+
if (this.shouldLog("warn")) {
|
|
536
|
+
console.error(this.formatMessage("warn", message, data));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
info(message, data) {
|
|
540
|
+
if (this.shouldLog("info")) {
|
|
541
|
+
console.error(this.formatMessage("info", message, data));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
debug(message, data) {
|
|
545
|
+
if (this.shouldLog("debug")) {
|
|
546
|
+
console.error(this.formatMessage("debug", message, data));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
trace(message, data) {
|
|
550
|
+
if (this.shouldLog("trace")) {
|
|
551
|
+
console.error(this.formatMessage("trace", message, data));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Log tool invocation with parameters
|
|
556
|
+
*/
|
|
557
|
+
toolInvoked(toolName, params, userId) {
|
|
558
|
+
this.info(`Tool invoked: ${toolName}`);
|
|
559
|
+
this.debug("Tool parameters", { tool: toolName, params, userId });
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Log tool completion with result summary
|
|
563
|
+
*/
|
|
564
|
+
toolCompleted(toolName, duration, resultSize) {
|
|
565
|
+
this.info(`Tool completed: ${toolName}`);
|
|
566
|
+
this.debug("Tool performance", { tool: toolName, duration: `${duration}ms`, resultSize });
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Log tool failure with error details
|
|
570
|
+
*/
|
|
571
|
+
toolFailed(toolName, error, params) {
|
|
572
|
+
this.error(`Tool execution failed: ${toolName}`, {
|
|
573
|
+
tool: toolName,
|
|
574
|
+
error: error.message,
|
|
575
|
+
stack: error.stack,
|
|
576
|
+
params
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Log SSH command execution
|
|
581
|
+
*/
|
|
582
|
+
sshCommand(command, cwd, timeout) {
|
|
583
|
+
this.debug("Executing SSH command", { command, cwd, timeout });
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Log SSH command result
|
|
587
|
+
*/
|
|
588
|
+
sshCommandResult(exitCode, duration, stdoutSize, stderrSize) {
|
|
589
|
+
this.debug("SSH command completed", {
|
|
590
|
+
exitCode,
|
|
591
|
+
duration: `${duration}ms`,
|
|
592
|
+
stdout: `${stdoutSize} bytes`,
|
|
593
|
+
stderr: `${stderrSize} bytes`
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Log file operation
|
|
598
|
+
*/
|
|
599
|
+
fileOperation(operation, path, details) {
|
|
600
|
+
this.info(`File operation: ${operation}`, { path, ...details });
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
var logger = new Logger();
|
|
604
|
+
|
|
498
605
|
// src/tools/acp-remote-execute-command.ts
|
|
499
606
|
var acpRemoteExecuteCommandTool = {
|
|
500
607
|
name: "acp_remote_execute_command",
|
|
@@ -521,9 +628,16 @@ var acpRemoteExecuteCommandTool = {
|
|
|
521
628
|
};
|
|
522
629
|
async function handleAcpRemoteExecuteCommand(args, sshConnection) {
|
|
523
630
|
const { command, cwd, timeout = 30 } = args;
|
|
631
|
+
logger.debug("Executing remote command", { command, cwd, timeout });
|
|
524
632
|
try {
|
|
525
633
|
const fullCommand = cwd ? `cd ${cwd} && ${command}` : command;
|
|
526
634
|
const result = await sshConnection.execWithTimeout(fullCommand, timeout);
|
|
635
|
+
logger.debug("Command execution result", {
|
|
636
|
+
exitCode: result.exitCode,
|
|
637
|
+
timedOut: result.timedOut,
|
|
638
|
+
stdoutLength: result.stdout.length,
|
|
639
|
+
stderrLength: result.stderr.length
|
|
640
|
+
});
|
|
527
641
|
const output = {
|
|
528
642
|
stdout: result.stdout,
|
|
529
643
|
stderr: result.stderr,
|
|
@@ -540,6 +654,7 @@ async function handleAcpRemoteExecuteCommand(args, sshConnection) {
|
|
|
540
654
|
};
|
|
541
655
|
} catch (error) {
|
|
542
656
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
657
|
+
logger.error("Command execution error", { command, error: errorMessage });
|
|
543
658
|
return {
|
|
544
659
|
content: [
|
|
545
660
|
{
|
|
@@ -584,8 +699,10 @@ var acpRemoteReadFileTool = {
|
|
|
584
699
|
};
|
|
585
700
|
async function handleAcpRemoteReadFile(args, sshConnection) {
|
|
586
701
|
const { path, encoding = "utf-8", maxSize = 1048576 } = args;
|
|
702
|
+
logger.debug("Reading remote file", { path, encoding, maxSize });
|
|
587
703
|
try {
|
|
588
704
|
const result = await sshConnection.readFile(path, encoding, maxSize);
|
|
705
|
+
logger.debug("File read successful", { path, size: result.size });
|
|
589
706
|
const output = {
|
|
590
707
|
content: result.content,
|
|
591
708
|
size: result.size,
|
|
@@ -601,6 +718,7 @@ async function handleAcpRemoteReadFile(args, sshConnection) {
|
|
|
601
718
|
};
|
|
602
719
|
} catch (error) {
|
|
603
720
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
721
|
+
logger.error("File read error", { path, error: errorMessage });
|
|
604
722
|
return {
|
|
605
723
|
content: [
|
|
606
724
|
{
|
|
@@ -653,12 +771,14 @@ var acpRemoteWriteFileTool = {
|
|
|
653
771
|
};
|
|
654
772
|
async function handleAcpRemoteWriteFile(args, sshConnection) {
|
|
655
773
|
const { path, content, encoding = "utf-8", createDirs = false, backup = false } = args;
|
|
774
|
+
logger.debug("Writing remote file", { path, contentSize: content.length, encoding, createDirs, backup });
|
|
656
775
|
try {
|
|
657
776
|
const result = await sshConnection.writeFile(path, content, {
|
|
658
777
|
encoding,
|
|
659
778
|
createDirs,
|
|
660
779
|
backup
|
|
661
780
|
});
|
|
781
|
+
logger.debug("File write successful", { path, bytesWritten: result.bytesWritten, backupPath: result.backupPath });
|
|
662
782
|
const output = {
|
|
663
783
|
success: result.success,
|
|
664
784
|
bytesWritten: result.bytesWritten,
|
|
@@ -674,6 +794,7 @@ async function handleAcpRemoteWriteFile(args, sshConnection) {
|
|
|
674
794
|
};
|
|
675
795
|
} catch (error) {
|
|
676
796
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
797
|
+
logger.error("File write error", { path, error: errorMessage });
|
|
677
798
|
return {
|
|
678
799
|
content: [
|
|
679
800
|
{
|
|
@@ -704,13 +825,27 @@ var SSHConnectionManager = class {
|
|
|
704
825
|
*/
|
|
705
826
|
async connect() {
|
|
706
827
|
if (this.connected) {
|
|
828
|
+
logger.debug("SSH connection already established");
|
|
707
829
|
return;
|
|
708
830
|
}
|
|
831
|
+
logger.info("Connecting to SSH server", {
|
|
832
|
+
host: this.config.host,
|
|
833
|
+
port: this.config.port || 22,
|
|
834
|
+
username: this.config.username
|
|
835
|
+
});
|
|
709
836
|
return new Promise((resolve, reject) => {
|
|
710
837
|
this.client.on("ready", () => {
|
|
711
838
|
this.connected = true;
|
|
839
|
+
logger.info("SSH connection established", {
|
|
840
|
+
host: this.config.host,
|
|
841
|
+
username: this.config.username
|
|
842
|
+
});
|
|
712
843
|
resolve();
|
|
713
844
|
}).on("error", (err) => {
|
|
845
|
+
logger.error("SSH connection failed", {
|
|
846
|
+
host: this.config.host,
|
|
847
|
+
error: err.message
|
|
848
|
+
});
|
|
714
849
|
reject(err);
|
|
715
850
|
}).connect({
|
|
716
851
|
host: this.config.host,
|
|
@@ -756,6 +891,8 @@ var SSHConnectionManager = class {
|
|
|
756
891
|
if (!this.connected) {
|
|
757
892
|
await this.connect();
|
|
758
893
|
}
|
|
894
|
+
const startTime = Date.now();
|
|
895
|
+
logger.sshCommand(command, void 0, timeoutSeconds);
|
|
759
896
|
const execPromise = new Promise((resolve, reject) => {
|
|
760
897
|
this.client.exec(command, (err, stream) => {
|
|
761
898
|
if (err) {
|
|
@@ -765,6 +902,8 @@ var SSHConnectionManager = class {
|
|
|
765
902
|
let stdout = "";
|
|
766
903
|
let stderr = "";
|
|
767
904
|
stream.on("close", (code) => {
|
|
905
|
+
const duration = Date.now() - startTime;
|
|
906
|
+
logger.sshCommandResult(code, duration, stdout.length, stderr.length);
|
|
768
907
|
resolve({ stdout, stderr, exitCode: code });
|
|
769
908
|
}).on("data", (data) => {
|
|
770
909
|
stdout += data.toString();
|
|
@@ -783,6 +922,7 @@ var SSHConnectionManager = class {
|
|
|
783
922
|
return { ...result, timedOut: false };
|
|
784
923
|
} catch (error) {
|
|
785
924
|
if (error instanceof Error && error.message === "Command execution timed out") {
|
|
925
|
+
logger.warn("SSH command timed out", { command, timeout: timeoutSeconds });
|
|
786
926
|
return {
|
|
787
927
|
stdout: "",
|
|
788
928
|
stderr: "Command execution timed out",
|
|
@@ -790,6 +930,10 @@ var SSHConnectionManager = class {
|
|
|
790
930
|
timedOut: true
|
|
791
931
|
};
|
|
792
932
|
}
|
|
933
|
+
logger.error("SSH command execution failed", {
|
|
934
|
+
command,
|
|
935
|
+
error: error instanceof Error ? error.message : String(error)
|
|
936
|
+
});
|
|
793
937
|
throw error;
|
|
794
938
|
}
|
|
795
939
|
}
|
|
@@ -833,22 +977,30 @@ var SSHConnectionManager = class {
|
|
|
833
977
|
* Read file contents from remote machine
|
|
834
978
|
*/
|
|
835
979
|
async readFile(path, encoding = "utf-8", maxSize = 1048576) {
|
|
980
|
+
const startTime = Date.now();
|
|
981
|
+
logger.fileOperation("read", path, { encoding, maxSize });
|
|
836
982
|
const sftp = await this.getSFTP();
|
|
837
983
|
return new Promise((resolve, reject) => {
|
|
838
984
|
sftp.stat(path, (err, stats) => {
|
|
839
985
|
if (err) {
|
|
986
|
+
logger.error("File stat failed", { path, error: err.message });
|
|
840
987
|
reject(new Error(`File not found or inaccessible: ${path}`));
|
|
841
988
|
return;
|
|
842
989
|
}
|
|
990
|
+
logger.debug("File stat retrieved", { path, size: stats.size });
|
|
843
991
|
if (stats.size > maxSize) {
|
|
992
|
+
logger.warn("File too large", { path, size: stats.size, maxSize });
|
|
844
993
|
reject(new Error(`File too large: ${stats.size} bytes (max: ${maxSize} bytes)`));
|
|
845
994
|
return;
|
|
846
995
|
}
|
|
847
996
|
sftp.readFile(path, { encoding }, (err2, data) => {
|
|
848
997
|
if (err2) {
|
|
998
|
+
logger.error("File read failed", { path, error: err2.message });
|
|
849
999
|
reject(new Error(`Failed to read file: ${err2.message}`));
|
|
850
1000
|
return;
|
|
851
1001
|
}
|
|
1002
|
+
const duration = Date.now() - startTime;
|
|
1003
|
+
logger.debug("File read completed", { path, size: stats.size, duration: `${duration}ms` });
|
|
852
1004
|
resolve({
|
|
853
1005
|
content: data.toString(),
|
|
854
1006
|
size: stats.size,
|
|
@@ -863,6 +1015,13 @@ var SSHConnectionManager = class {
|
|
|
863
1015
|
*/
|
|
864
1016
|
async writeFile(path, content, options = {}) {
|
|
865
1017
|
const { encoding = "utf-8", createDirs = false, backup = false } = options;
|
|
1018
|
+
const startTime = Date.now();
|
|
1019
|
+
logger.fileOperation("write", path, {
|
|
1020
|
+
contentSize: content.length,
|
|
1021
|
+
encoding,
|
|
1022
|
+
createDirs,
|
|
1023
|
+
backup
|
|
1024
|
+
});
|
|
866
1025
|
const sftp = await this.getSFTP();
|
|
867
1026
|
return new Promise((resolve, reject) => {
|
|
868
1027
|
const writeOperation = () => {
|
|
@@ -889,9 +1048,17 @@ var SSHConnectionManager = class {
|
|
|
889
1048
|
}
|
|
890
1049
|
sftp.rename(tempPath, path, (err2) => {
|
|
891
1050
|
if (err2) {
|
|
1051
|
+
logger.error("File rename failed", { tempPath, path, error: err2.message });
|
|
892
1052
|
reject(new Error(`Failed to rename temp file: ${err2.message}`));
|
|
893
1053
|
return;
|
|
894
1054
|
}
|
|
1055
|
+
const duration = Date.now() - startTime;
|
|
1056
|
+
logger.debug("File write completed", {
|
|
1057
|
+
path,
|
|
1058
|
+
bytesWritten: buffer.length,
|
|
1059
|
+
duration: `${duration}ms`,
|
|
1060
|
+
backupPath
|
|
1061
|
+
});
|
|
895
1062
|
resolve({
|
|
896
1063
|
success: true,
|
|
897
1064
|
bytesWritten: buffer.length,
|
|
@@ -915,6 +1082,10 @@ var SSHConnectionManager = class {
|
|
|
915
1082
|
*/
|
|
916
1083
|
disconnect() {
|
|
917
1084
|
if (this.connected) {
|
|
1085
|
+
logger.info("Disconnecting from SSH server", {
|
|
1086
|
+
host: this.config.host,
|
|
1087
|
+
username: this.config.username
|
|
1088
|
+
});
|
|
918
1089
|
this.client.end();
|
|
919
1090
|
this.connected = false;
|
|
920
1091
|
}
|
|
@@ -949,26 +1120,39 @@ async function main() {
|
|
|
949
1120
|
}
|
|
950
1121
|
}
|
|
951
1122
|
);
|
|
952
|
-
server.setRequestHandler(ListToolsRequestSchema, async () =>
|
|
953
|
-
|
|
954
|
-
|
|
1123
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1124
|
+
logger.debug("Tool discovery requested");
|
|
1125
|
+
const tools = [acpRemoteListFilesTool, acpRemoteExecuteCommandTool, acpRemoteReadFileTool, acpRemoteWriteFileTool];
|
|
1126
|
+
logger.debug(`Returning ${tools.length} tools`, { tools: tools.map((t) => t.name) });
|
|
1127
|
+
return { tools };
|
|
1128
|
+
});
|
|
955
1129
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1130
|
+
const startTime = Date.now();
|
|
1131
|
+
logger.toolInvoked(request.params.name, request.params.arguments);
|
|
1132
|
+
try {
|
|
1133
|
+
let result;
|
|
1134
|
+
if (request.params.name === "acp_remote_list_files") {
|
|
1135
|
+
result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
|
|
1136
|
+
} else if (request.params.name === "acp_remote_execute_command") {
|
|
1137
|
+
result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection);
|
|
1138
|
+
} else if (request.params.name === "acp_remote_read_file") {
|
|
1139
|
+
result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
|
|
1140
|
+
} else if (request.params.name === "acp_remote_write_file") {
|
|
1141
|
+
result = await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
|
|
1142
|
+
} else {
|
|
1143
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1144
|
+
}
|
|
1145
|
+
const duration = Date.now() - startTime;
|
|
1146
|
+
const resultSize = JSON.stringify(result).length;
|
|
1147
|
+
logger.toolCompleted(request.params.name, duration, resultSize);
|
|
1148
|
+
return result;
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
logger.toolFailed(request.params.name, error, request.params.arguments);
|
|
1151
|
+
throw error;
|
|
967
1152
|
}
|
|
968
|
-
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
969
1153
|
});
|
|
970
1154
|
const cleanup = () => {
|
|
971
|
-
|
|
1155
|
+
logger.info("Shutting down server");
|
|
972
1156
|
sshConnection.disconnect();
|
|
973
1157
|
process.exit(0);
|
|
974
1158
|
};
|
|
@@ -976,10 +1160,10 @@ async function main() {
|
|
|
976
1160
|
process.on("SIGTERM", cleanup);
|
|
977
1161
|
const transport = new StdioServerTransport();
|
|
978
1162
|
await server.connect(transport);
|
|
979
|
-
|
|
1163
|
+
logger.info("ACP MCP Server running on stdio");
|
|
980
1164
|
}
|
|
981
1165
|
main().catch((error) => {
|
|
982
|
-
|
|
1166
|
+
logger.error("Server startup failed", { error: error.message, stack: error.stack });
|
|
983
1167
|
process.exit(1);
|
|
984
1168
|
});
|
|
985
1169
|
//# sourceMappingURL=server.js.map
|