ftp-mcp 1.3.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/index.js +258 -103
  3. package/package.json +122 -122
package/README.md CHANGED
@@ -9,7 +9,8 @@ An enterprise-grade Model Context Protocol (MCP) server providing sophisticated
9
9
  - **Security & Authorization**: Native support for SSH Key exchanges (both direct and Agent-forwarding), Pass-phrase authentication, and explicit `readOnly: true` config profiling to sandbox dangerous modifications.
10
10
  - **Smart Directory Syncing**: Deep hash-and-size sync algorithms for minimal network payload deployments, complete with `--dryRun` toggling that logs differences back to the LLM without modifying live directories.
11
11
  - **Git & Node Aware**: Automatically unpacks `.gitignore` and `.ftpignore` environments. Semantically evaluates `package.json` for smart context summarization.
12
- - **Interactive Initializer**: Simple scaffolding of configurations natively via the CLI.
12
+ - **AI-Guided Initialization**: A dedicated "AI-First" configuration track (`--init`) that provides highly verbose instruction context during setup, ensuring AI assistants absorb server capabilities and operational constraints directly into their working memory.
13
+ - **Interactive Initializer**: Simple scaffolding of configurations natively via the CLI for human users.
13
14
  - **Audit Logging**: Robust instrumentation generating structured `.ftp-mcp-audit.log` traces on all filesystem mutations.
14
15
  - **Comprehensive E2E Testing**: Guaranteed operational functionality validated continuously by Node-based MCP IO test orchestration.
15
16
 
package/index.js CHANGED
@@ -5,6 +5,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
  import {
6
6
  CallToolRequestSchema,
7
7
  ListToolsRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema,
8
10
  } from "@modelcontextprotocol/sdk/types.js";
9
11
  import { Client as FTPClient } from "basic-ftp";
10
12
  import SFTPClient from "ssh2-sftp-client";
@@ -25,8 +27,41 @@ import crypto from "crypto";
25
27
  const __filename = fileURLToPath(import.meta.url);
26
28
  const __dirname = path.dirname(__filename);
27
29
 
30
+ // AI-First Semantic Icons
31
+ const ICON = {
32
+ DIR: "📂",
33
+ FILE: "📄",
34
+ PKG: "📦",
35
+ CONFIG: "⚙️",
36
+ SECRET: "🔒",
37
+ BACKUP: "🕰️",
38
+ HINT: "💡",
39
+ ERROR: "❌"
40
+ };
41
+
42
+ /**
43
+ * AI-First: Generate context-aware suggestions for troubleshooting and next steps.
44
+ * This helper ensures the LLM receives actionable, backticked commands.
45
+ */
46
+ function getAISuggestion(type, context = {}) {
47
+ switch (type) {
48
+ case 'error_enoent':
49
+ return `[AI: Path not found. Suggested fix: Check your CWD with \`ftp_list "."\` or verify the path exists with \`ftp_exists "${context.path}"\`]`;
50
+ case 'error_permission':
51
+ return `[AI: Permission denied. Suggested fix: Verify user rights with \`ftp_stat "${context.path}"\` or check if the server supports \`ftp_chmod\`]`;
52
+ case 'hint_connected':
53
+ return `[HINT: Connection active. Suggested next step: Run \`ftp_analyze_workspace "."\` to understand the project architecture.]`;
54
+ case 'hint_list_config':
55
+ return `[HINT: Found project manifests. Suggested next step: Read \`package.json\` using \`ftp_get_contents "package.json"\` to see dependencies.]`;
56
+ case 'hint_destructive_readonly':
57
+ return `[AI: Server is in READ-ONLY mode. Use \`ftp_sync --dryRun\` to simulate this deployment instead.]`;
58
+ default:
59
+ return null;
60
+ }
61
+ }
62
+
28
63
  // Read version from package.json to avoid version drift (CODE-1)
29
- let SERVER_VERSION = "1.3.0";
64
+ let SERVER_VERSION = "1.4.0";
30
65
  try {
31
66
  const pkg = JSON.parse(readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
32
67
  SERVER_VERSION = pkg.version || SERVER_VERSION;
@@ -42,27 +77,50 @@ if (process.argv.includes("--init")) {
42
77
 
43
78
  intro('🚀 Welcome to FTP-MCP Initialization Wizard');
44
79
 
80
+ const setupType = await select({
81
+ message: 'Who is running this setup wizard?',
82
+ options: [
83
+ { value: 'ai', label: '1. AI Agent Install (Provides verbose context on how to use the server)', hint: 'Detailed instructions for LLMs' },
84
+ { value: 'human', label: '2. Human Install (Standard Setup)', hint: 'Concise standard setup for human users' }
85
+ ]
86
+ });
87
+ if (isCancel(setupType)) { outro('Setup cancelled.'); process.exit(0); }
88
+
89
+ const isAI = setupType === 'ai';
90
+
45
91
  const host = await text({
46
- message: 'Enter your FTP/SFTP Host (e.g. sftp://ftp.example.com)',
92
+ message: isAI
93
+ ? '[AI INSTRUCTION] Provide the remote FTP/SFTP Host address. (e.g. sftp://ftp.example.com or ftp://1.2.3.4). Note: This tool brokers remote filesystem access as an MCP server.'
94
+ : 'Enter your FTP/SFTP Host (e.g. sftp://ftp.example.com)',
47
95
  placeholder: 'sftp://127.0.0.1',
48
- validate: (val) => val.length === 0 ? "Host is required!" : undefined,
96
+ validate: (val) => (!val || val.length === 0) ? "Host is required!" : undefined,
49
97
  });
50
98
  if (isCancel(host)) { outro('Setup cancelled.'); process.exit(0); }
51
99
 
52
100
  const user = await text({
53
- message: 'Enter your Username',
54
- validate: (val) => val.length === 0 ? "User is required!" : undefined,
101
+ message: isAI
102
+ ? '[AI INSTRUCTION] Provide the remote server username. This credential will be used for all subsequent tool calls in this session.'
103
+ : 'Enter your Username',
104
+ validate: (val) => (!val || val.length === 0) ? "User is required!" : undefined,
55
105
  });
56
106
  if (isCancel(user)) { outro('Setup cancelled.'); process.exit(0); }
57
107
 
58
108
  const pass = await promptPassword({
59
- message: 'Enter your Password (optional if using keys)',
109
+ message: isAI
110
+ ? '[AI INSTRUCTION] Provide the password for the connection. If you are using SSH key authentication for SFTP, you may leave this field empty.'
111
+ : 'Enter your Password (optional if using keys)',
60
112
  });
61
113
  if (isCancel(pass)) { outro('Setup cancelled.'); process.exit(0); }
62
114
 
63
115
  const port = await text({
64
- message: 'Enter port (optional, defaults to 21 for FTP, 22 for SFTP)',
65
- placeholder: '22'
116
+ message: isAI
117
+ ? '[AI INSTRUCTION] Provide the specific port for the connection. Default is 21 for FTP and 22 for SFTP. If the user has not specified a custom port, you should leave this blank.'
118
+ : 'Enter port (optional, defaults to 21 for FTP, 22 for SFTP)',
119
+ placeholder: '22',
120
+ validate: (val) => {
121
+ if (val && isNaN(parseInt(val, 10))) return "Port must be a number!";
122
+ return undefined;
123
+ }
66
124
  });
67
125
  if (isCancel(port)) { outro('Setup cancelled.'); process.exit(0); }
68
126
 
@@ -70,10 +128,17 @@ if (process.argv.includes("--init")) {
70
128
  let privateKey = '';
71
129
 
72
130
  if (isSFTP) {
73
- const usesKey = await confirm({ message: 'Are you using an SSH Private Key instead of a password?' });
74
- if (!isCancel(usesKey) && usesKey) {
131
+ const usesKey = await confirm({
132
+ message: isAI
133
+ ? '[AI INSTRUCTION] An SFTP protocol was detected. Is the user employing an SSH Private Key instead of a traditional password? (Recommended for production)'
134
+ : 'Are you using an SSH Private Key instead of a password?'
135
+ });
136
+ if (isCancel(usesKey)) { outro('Setup cancelled.'); process.exit(0); }
137
+ if (usesKey) {
75
138
  privateKey = await text({
76
- message: 'Path to your private key (e.g. ~/.ssh/id_rsa)',
139
+ message: isAI
140
+ ? '[AI INSTRUCTION] Provide the absolute local filesystem path to the SSH private key (e.g. ~/.ssh/id_rsa). This path must be accessible from the environment where the MCP server is running.'
141
+ : 'Path to your private key (e.g. ~/.ssh/id_rsa)',
77
142
  });
78
143
  if (isCancel(privateKey)) { outro('Setup cancelled.'); process.exit(0); }
79
144
  }
@@ -92,9 +157,26 @@ if (process.argv.includes("--init")) {
92
157
 
93
158
  await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
94
159
 
95
- note(`✅ Successfully generated config file at:\n${CONFIG_FILE}`, 'Success');
160
+ if (isAI) {
161
+ note(
162
+ "AI CONTEXT ESTABLISHED: The `.ftpconfig` file has been generated.\n" +
163
+ "You now have access to this MCP server's tools.\n\n" +
164
+ "Important capabilities:\n" +
165
+ "- Content-rich operations: `ftp_list`, `ftp_download`, `ftp_upload`.\n" +
166
+ "- Batched operations: `ftp_batch_upload`, `ftp_batch_download` (Highly recommended for speed).\n" +
167
+ "- Safe regex searches: `ftp_search`.\n" +
168
+ "- Transparent paths: All remote operations are relative to the server root.\n\n" +
169
+ "You may now proceed with the user's tasks.",
170
+ "Configuration Success"
171
+ );
172
+ } else {
173
+ note(`✅ Successfully generated config file at:\n${CONFIG_FILE}`, 'Success');
174
+ }
96
175
 
97
- outro("You're ready to deploy with MCP! Ask your AI to 'list remote files'");
176
+ outro(isAI
177
+ ? "Deployment complete. You are now configured to manage the remote filesystem."
178
+ : "You're ready to deploy with MCP! Ask your AI to 'list remote files'"
179
+ );
98
180
  } catch (err) {
99
181
  console.error(`❌ Init failed: ${err.message}`);
100
182
  }
@@ -104,6 +186,7 @@ if (process.argv.includes("--init")) {
104
186
 
105
187
  let currentConfig = null;
106
188
  let currentProfile = null;
189
+ let sessionHintShown = false;
107
190
 
108
191
  const DEFAULT_IGNORE_PATTERNS = [
109
192
  'node_modules/**',
@@ -739,11 +822,12 @@ function generateSemanticPreview(filesToChange) {
739
822
  const server = new Server(
740
823
  {
741
824
  name: "ftp-mcp-server",
742
- version: SERVER_VERSION, // CODE-1: reads from package.json at startup
825
+ version: SERVER_VERSION,
743
826
  },
744
827
  {
745
828
  capabilities: {
746
829
  tools: {},
830
+ resources: {},
747
831
  },
748
832
  }
749
833
  );
@@ -753,17 +837,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
753
837
  tools: [
754
838
  {
755
839
  name: "ftp_connect",
756
- description: "Connect to a named FTP profile from .ftpconfig",
840
+ description: "Establish or switch the active connection to a specific remote server profile defined in your .ftpconfig (e.g., 'production', 'staging'). This is the first step before performing any remote operations.",
757
841
  inputSchema: {
758
842
  type: "object",
759
843
  properties: {
760
844
  profile: {
761
845
  type: "string",
762
- description: "Profile name from .ftpconfig (e.g., 'production', 'staging')"
846
+ description: "The named profile key from your .ftpconfig file."
763
847
  },
764
848
  useEnv: {
765
849
  type: "boolean",
766
- description: "Force use of environment variables instead of .ftpconfig",
850
+ description: "If true, bypasses .ftpconfig and connects using global environment variables (FTPMCP_HOST, etc.)",
767
851
  default: false
768
852
  }
769
853
  }
@@ -771,13 +855,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
771
855
  },
772
856
  {
773
857
  name: "ftp_deploy",
774
- description: "Run a named deployment preset from .ftpconfig",
858
+ description: "Execute a pre-defined deployment preset from .ftpconfig. This typically maps a specific local folder to a remote target with pre-configured exclusion rules.",
775
859
  inputSchema: {
776
860
  type: "object",
777
861
  properties: {
778
862
  deployment: {
779
863
  type: "string",
780
- description: "Deployment name from .ftpconfig deployments (e.g., 'deploy-frontend', 'deploy-api')"
864
+ description: "The name of the deployment preset (e.g., 'web-app', 'api-server')."
781
865
  }
782
866
  },
783
867
  required: ["deployment"]
@@ -793,23 +877,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
793
877
  },
794
878
  {
795
879
  name: "ftp_list",
796
- description: "List files and directories in a remote FTP/SFTP path",
880
+ description: "List files and directories in a remote path. Use 'limit' and 'offset' for pagination when dealing with directories containing hundreds of files to avoid context overflow.",
797
881
  inputSchema: {
798
882
  type: "object",
799
883
  properties: {
800
884
  path: {
801
885
  type: "string",
802
- description: "Remote path to list (defaults to current directory)",
886
+ description: "Remote directory path (defaults to current working directory).",
803
887
  default: "."
804
888
  },
805
889
  limit: {
806
890
  type: "number",
807
- description: "Maximum number of files to return",
891
+ description: "Maximum results to return in this chunk.",
808
892
  default: 100
809
893
  },
810
894
  offset: {
811
895
  type: "number",
812
- description: "Number of files to skip over",
896
+ description: "Starting position in the file list for pagination.",
813
897
  default: 0
814
898
  }
815
899
  }
@@ -817,21 +901,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
817
901
  },
818
902
  {
819
903
  name: "ftp_get_contents",
820
- description: "Read file content directly from FTP/SFTP without downloading",
904
+ description: "Read the source text of a remote file. CRITICAL: For large files, use 'startLine' and 'endLine' to extract specific chunks and prevent hitting the LLM context limit.",
821
905
  inputSchema: {
822
906
  type: "object",
823
907
  properties: {
824
908
  path: {
825
909
  type: "string",
826
- description: "Remote file path to read"
910
+ description: "Absolute or relative remote path to the file."
827
911
  },
828
912
  startLine: {
829
913
  type: "number",
830
- description: "Optional start line for reading chunk (1-indexed)"
914
+ description: "Optional: The first line to include in the output (1-indexed)."
831
915
  },
832
916
  endLine: {
833
917
  type: "number",
834
- description: "Optional end line for reading chunk (inclusive, 1-indexed)"
918
+ description: "Optional: The last line to include (inclusive, 1-indexed)."
835
919
  }
836
920
  },
837
921
  required: ["path"]
@@ -839,25 +923,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
839
923
  },
840
924
  {
841
925
  name: "ftp_patch_file",
842
- description: "Apply a Unified Diff patch to a remote file",
926
+ description: "Apply a Unified Diff patch to a remote file. RECOMMENDED: Use this instead of ftp_put_contents for updating existing files to minimize bandwidth and ensure atomic-like updates. For new files, use ftp_put_contents.",
843
927
  inputSchema: {
844
928
  type: "object",
845
929
  properties: {
846
930
  path: {
847
931
  type: "string",
848
- description: "Remote file path to patch"
932
+ description: "Remote path to the existing file to be patched."
849
933
  },
850
934
  patch: {
851
935
  type: "string",
852
- description: "Unified diff string containing the changes"
936
+ description: "The Unified Diff formatted string containing your local changes."
853
937
  },
854
938
  expectedHash: {
855
939
  type: "string",
856
- description: "Optional MD5 hash of the file before patching to prevent drift"
940
+ description: "Optional (but recommended): The SHA-256 hash of the remote file before patching to prevent race conditions (drift protection)."
857
941
  },
858
942
  createBackup: {
859
943
  type: "boolean",
860
- description: "Create a .bak file before patching",
944
+ description: "Generate a .bak copy of the remote file before applying the changes.",
861
945
  default: true
862
946
  }
863
947
  },
@@ -866,13 +950,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
866
950
  },
867
951
  {
868
952
  name: "ftp_analyze_workspace",
869
- description: "Semantically analyze a remote directory to detect project type and dependencies",
953
+ description: "Introspect the remote directory to identify technical environments (e.g., Node.js, PHP, Python) and read dependency manifests. Use this to gain architectural context of a new codebase.",
870
954
  inputSchema: {
871
955
  type: "object",
872
956
  properties: {
873
957
  path: {
874
958
  type: "string",
875
- description: "Remote directory path to analyze (defaults to current)",
959
+ description: "Remote directory to analyze (defaults to current server root).",
876
960
  default: "."
877
961
  }
878
962
  }
@@ -880,17 +964,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
880
964
  },
881
965
  {
882
966
  name: "ftp_put_contents",
883
- description: "Write content directly to FTP/SFTP file without local file",
967
+ description: "Write raw text directly to a remote destination. Best for creating NEW files. For modifying existing files, prefer ftp_patch_file.",
884
968
  inputSchema: {
885
969
  type: "object",
886
970
  properties: {
887
971
  path: {
888
972
  type: "string",
889
- description: "Remote file path to write"
973
+ description: "Remote destination path."
890
974
  },
891
975
  content: {
892
976
  type: "string",
893
- description: "Content to write to the file"
977
+ description: "The full string content to write."
894
978
  }
895
979
  },
896
980
  required: ["path", "content"]
@@ -898,13 +982,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
898
982
  },
899
983
  {
900
984
  name: "ftp_stat",
901
- description: "Get file metadata (size, modified date, permissions)",
985
+ description: "Retrieve comprehensive metadata for a remote property, including size, modification timestamps, and UNIX permissions.",
902
986
  inputSchema: {
903
987
  type: "object",
904
988
  properties: {
905
989
  path: {
906
990
  type: "string",
907
- description: "Remote file path"
991
+ description: "Remote file or directory path."
908
992
  }
909
993
  },
910
994
  required: ["path"]
@@ -912,13 +996,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
912
996
  },
913
997
  {
914
998
  name: "ftp_exists",
915
- description: "Check if file or folder exists on FTP/SFTP server",
999
+ description: "Check for the existence of a file or folder without performing heavy file operations. Use this for conditional logic workflows.",
916
1000
  inputSchema: {
917
1001
  type: "object",
918
1002
  properties: {
919
1003
  path: {
920
1004
  type: "string",
921
- description: "Remote path to check"
1005
+ description: "Remote target path."
922
1006
  }
923
1007
  },
924
1008
  required: ["path"]
@@ -926,18 +1010,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
926
1010
  },
927
1011
  {
928
1012
  name: "ftp_tree",
929
- description: "Get recursive directory listing (entire structure at once)",
1013
+ description: "Generate a complete recursive directory map. Use this to visualize project structure, but be cautious with 'maxDepth' in very large remote repositories to avoid excessive network payload.",
930
1014
  inputSchema: {
931
1015
  type: "object",
932
1016
  properties: {
933
1017
  path: {
934
1018
  type: "string",
935
- description: "Remote path to start tree from",
1019
+ description: "Remote directory path to start mapping from (defaults to root).",
936
1020
  default: "."
937
1021
  },
938
1022
  maxDepth: {
939
1023
  type: "number",
940
- description: "Maximum depth to recurse",
1024
+ description: "Maximum recursion depth to prevent infinite loops or huge payloads.",
941
1025
  default: 10
942
1026
  }
943
1027
  }
@@ -945,40 +1029,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
945
1029
  },
946
1030
  {
947
1031
  name: "ftp_search",
948
- description: "Advanced remote search: find files by name, content, or type",
1032
+ description: "Advanced remote file search. Supports finding files by name (wildcards), extension, or content regex (grep-like). Use this to find specific code patterns across the remote workspace.",
949
1033
  inputSchema: {
950
1034
  type: "object",
951
1035
  properties: {
952
1036
  pattern: {
953
1037
  type: "string",
954
- description: "Filename search pattern (supports wildcards like *.js)"
1038
+ description: "File name pattern (e.g. `*.js`, `db_*`)."
955
1039
  },
956
1040
  contentPattern: {
957
1041
  type: "string",
958
- description: "Regex pattern to search inside file contents (grep)"
1042
+ description: "Regex pattern to search inside file contents. Highly efficient for finding variable usage or specific logic."
959
1043
  },
960
1044
  extension: {
961
1045
  type: "string",
962
- description: "Filter by file extension (e.g., '.js', '.php')"
1046
+ description: "Restrict search to specific file extensions (e.g. `.css`)."
963
1047
  },
964
1048
  findLikelyConfigs: {
965
1049
  type: "boolean",
966
- description: "If true, prioritizes finding config, auth, and build files",
1050
+ description: "Prioritize searching for project manifests and config files (package.json, .env, etc.)",
967
1051
  default: false
968
1052
  },
969
1053
  path: {
970
1054
  type: "string",
971
- description: "Remote path to search in",
1055
+ description: "Remote directory to start the recursive search in.",
972
1056
  default: "."
973
1057
  },
974
1058
  limit: {
975
1059
  type: "number",
976
- description: "Maximum results to return",
1060
+ description: "Maximum matches to return.",
977
1061
  default: 50
978
1062
  },
979
1063
  offset: {
980
1064
  type: "number",
981
- description: "Results to skip over",
1065
+ description: "Skip initial matches for pagination.",
982
1066
  default: 0
983
1067
  }
984
1068
  }
@@ -986,17 +1070,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
986
1070
  },
987
1071
  {
988
1072
  name: "ftp_copy",
989
- description: "Duplicate files on server (SFTP only)",
1073
+ description: "Directly duplicate a file on the remote server without downloading and re-uploading. CRITICAL: Only supported on SFTP connections.",
990
1074
  inputSchema: {
991
1075
  type: "object",
992
1076
  properties: {
993
1077
  sourcePath: {
994
1078
  type: "string",
995
- description: "Source file path"
1079
+ description: "Qualified remote source path."
996
1080
  },
997
1081
  destPath: {
998
1082
  type: "string",
999
- description: "Destination file path"
1083
+ description: "Target remote destination path."
1000
1084
  }
1001
1085
  },
1002
1086
  required: ["sourcePath", "destPath"]
@@ -1004,18 +1088,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1004
1088
  },
1005
1089
  {
1006
1090
  name: "ftp_batch_upload",
1007
- description: "Upload multiple files at once",
1091
+ description: "Upload a collection of local files to remote destinations in a single operation. HIGHLY RECOMMENDED for multiple files to minimize connection handshaking overhead and drastically improve performance.",
1008
1092
  inputSchema: {
1009
1093
  type: "object",
1010
1094
  properties: {
1011
1095
  files: {
1012
1096
  type: "array",
1013
- description: "Array of {localPath, remotePath} objects",
1097
+ description: "A list of objects, each defining a local source and a remote destination.",
1014
1098
  items: {
1015
1099
  type: "object",
1016
1100
  properties: {
1017
- localPath: { type: "string" },
1018
- remotePath: { type: "string" }
1101
+ localPath: { type: "string", description: "Source path on your local machine." },
1102
+ remotePath: { type: "string", description: "Target path on the remote server." }
1019
1103
  },
1020
1104
  required: ["localPath", "remotePath"]
1021
1105
  }
@@ -1026,18 +1110,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1026
1110
  },
1027
1111
  {
1028
1112
  name: "ftp_batch_download",
1029
- description: "Download multiple files at once",
1113
+ description: "Download a selection of remote files to local destinations. HIGHLY RECOMMENDED for bulk downloads to leverage stable socket persistence.",
1030
1114
  inputSchema: {
1031
1115
  type: "object",
1032
1116
  properties: {
1033
1117
  files: {
1034
1118
  type: "array",
1035
- description: "Array of {remotePath, localPath} objects",
1119
+ description: "A list of objects mapping remote sources to local destinations.",
1036
1120
  items: {
1037
1121
  type: "object",
1038
1122
  properties: {
1039
- remotePath: { type: "string" },
1040
- localPath: { type: "string" }
1123
+ remotePath: { type: "string", description: "Source path on the remote server." },
1124
+ localPath: { type: "string", description: "Destination path on your local machine." }
1041
1125
  },
1042
1126
  required: ["remotePath", "localPath"]
1043
1127
  }
@@ -1048,33 +1132,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1048
1132
  },
1049
1133
  {
1050
1134
  name: "ftp_sync",
1051
- description: "Smart sync local remote (only changed files)",
1135
+ description: "Deploy entire project folders using smart synchronization. Analyzes local and remote directory trees and only transfers files that have changed in size or modification date. Automatically respects .gitignore and .ftpignore.",
1052
1136
  inputSchema: {
1053
1137
  type: "object",
1054
1138
  properties: {
1055
1139
  localPath: {
1056
1140
  type: "string",
1057
- description: "Local directory path"
1141
+ description: "The source directory on your local machine."
1058
1142
  },
1059
1143
  remotePath: {
1060
1144
  type: "string",
1061
- description: "Remote directory path"
1145
+ description: "The target destination directory on the remote server."
1062
1146
  },
1063
1147
  direction: {
1064
1148
  type: "string",
1065
- // QUAL-2: Only 'upload' is implemented; removed 'download'/'both' to avoid silent no-ops
1066
- description: "Sync direction: currently only 'upload' is supported",
1149
+ description: "Sync direction. Currently only 'upload' is implemented for safety.",
1067
1150
  enum: ["upload"],
1068
1151
  default: "upload"
1069
1152
  },
1070
1153
  dryRun: {
1071
1154
  type: "boolean",
1072
- description: "If true, simulates the sync without transferring files",
1155
+ description: "If true, logs exactly which files would be changed without performing any actual transfers.",
1073
1156
  default: false
1074
1157
  },
1075
1158
  useManifest: {
1076
1159
  type: "boolean",
1077
- description: "Use local manifest cache for faster deploys (drift-aware)",
1160
+ description: "Enables the local manifest cache for extremely fast delta detection on subsequent syncs.",
1078
1161
  default: true
1079
1162
  }
1080
1163
  },
@@ -1083,13 +1166,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1083
1166
  },
1084
1167
  {
1085
1168
  name: "ftp_disk_space",
1086
- description: "Check available space on server (SFTP only)",
1169
+ description: "Query the remote server for available disk space. CRITICAL: Only available on SFTP connections.",
1087
1170
  inputSchema: {
1088
1171
  type: "object",
1089
1172
  properties: {
1090
1173
  path: {
1091
1174
  type: "string",
1092
- description: "Remote path to check",
1175
+ description: "Remote filesystem path to check (defaults to server root).",
1093
1176
  default: "."
1094
1177
  }
1095
1178
  }
@@ -1097,17 +1180,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1097
1180
  },
1098
1181
  {
1099
1182
  name: "ftp_upload",
1100
- description: "Upload a file to the FTP/SFTP server",
1183
+ description: "Standard single-file transport to the remote server. For bulk transfers, favor ftp_batch_upload.",
1101
1184
  inputSchema: {
1102
1185
  type: "object",
1103
1186
  properties: {
1104
1187
  localPath: {
1105
1188
  type: "string",
1106
- description: "Local file path to upload"
1189
+ description: "Source path on your local machine."
1107
1190
  },
1108
1191
  remotePath: {
1109
1192
  type: "string",
1110
- description: "Remote destination path"
1193
+ description: "Target location on the remote server."
1111
1194
  }
1112
1195
  },
1113
1196
  required: ["localPath", "remotePath"]
@@ -1115,17 +1198,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1115
1198
  },
1116
1199
  {
1117
1200
  name: "ftp_download",
1118
- description: "Download a file from the FTP/SFTP server",
1201
+ description: "Standard single-file transport from the remote server. For bulk downloads, favor ftp_batch_download.",
1119
1202
  inputSchema: {
1120
1203
  type: "object",
1121
1204
  properties: {
1122
1205
  remotePath: {
1123
1206
  type: "string",
1124
- description: "Remote file path to download"
1207
+ description: "Source file location on the remote server."
1125
1208
  },
1126
1209
  localPath: {
1127
1210
  type: "string",
1128
- description: "Local destination path"
1211
+ description: "Destination location on your local machine."
1129
1212
  }
1130
1213
  },
1131
1214
  required: ["remotePath", "localPath"]
@@ -1133,13 +1216,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1133
1216
  },
1134
1217
  {
1135
1218
  name: "ftp_delete",
1136
- description: "Delete a file from the FTP/SFTP server",
1219
+ description: "Permanently remove a file from the remote server. Use with caution.",
1137
1220
  inputSchema: {
1138
1221
  type: "object",
1139
1222
  properties: {
1140
1223
  path: {
1141
1224
  type: "string",
1142
- description: "Remote file path to delete"
1225
+ description: "The remote file path to be destroyed."
1143
1226
  }
1144
1227
  },
1145
1228
  required: ["path"]
@@ -1147,13 +1230,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1147
1230
  },
1148
1231
  {
1149
1232
  name: "ftp_mkdir",
1150
- description: "Create a directory on the FTP/SFTP server",
1233
+ description: "Create a new directory structure on the remote server. Supports nested creation (mkdir -p).",
1151
1234
  inputSchema: {
1152
1235
  type: "object",
1153
1236
  properties: {
1154
1237
  path: {
1155
1238
  type: "string",
1156
- description: "Remote directory path to create"
1239
+ description: "The remote directory path to create."
1157
1240
  }
1158
1241
  },
1159
1242
  required: ["path"]
@@ -1161,17 +1244,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1161
1244
  },
1162
1245
  {
1163
1246
  name: "ftp_rmdir",
1164
- description: "Remove a directory from the FTP/SFTP server",
1247
+ description: "Delete a directory from the remote server.",
1165
1248
  inputSchema: {
1166
1249
  type: "object",
1167
1250
  properties: {
1168
1251
  path: {
1169
1252
  type: "string",
1170
- description: "Remote directory path to remove"
1253
+ description: "The remote directory path to remove."
1171
1254
  },
1172
1255
  recursive: {
1173
1256
  type: "boolean",
1174
- description: "Remove directory recursively",
1257
+ description: "If true, deletes all files and subdirectories within the target directory.",
1175
1258
  default: false
1176
1259
  }
1177
1260
  },
@@ -1180,17 +1263,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1180
1263
  },
1181
1264
  {
1182
1265
  name: "ftp_chmod",
1183
- description: "Change file permissions on the FTP/SFTP server (SFTP only)",
1266
+ description: "Change remote file permissions. CRITICAL: Only supported on SFTP connections.",
1184
1267
  inputSchema: {
1185
1268
  type: "object",
1186
1269
  properties: {
1187
1270
  path: {
1188
1271
  type: "string",
1189
- description: "Remote file path"
1272
+ description: "Remote file path to modify."
1190
1273
  },
1191
1274
  mode: {
1192
1275
  type: "string",
1193
- description: "Permission mode in octal (e.g., '755', '644')"
1276
+ description: "Standard octal permission string (e.g., '755' for executable, '644' for read-write)."
1194
1277
  }
1195
1278
  },
1196
1279
  required: ["path", "mode"]
@@ -1198,17 +1281,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1198
1281
  },
1199
1282
  {
1200
1283
  name: "ftp_rename",
1201
- description: "Rename or move a file on the FTP/SFTP server",
1284
+ description: "Move or rename files and directories on the remote server.",
1202
1285
  inputSchema: {
1203
1286
  type: "object",
1204
1287
  properties: {
1205
1288
  oldPath: {
1206
1289
  type: "string",
1207
- description: "Current file path"
1290
+ description: "Current remote location."
1208
1291
  },
1209
1292
  newPath: {
1210
1293
  type: "string",
1211
- description: "New file path"
1294
+ description: "New desired remote location."
1212
1295
  }
1213
1296
  },
1214
1297
  required: ["oldPath", "newPath"]
@@ -1216,13 +1299,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1216
1299
  },
1217
1300
  {
1218
1301
  name: "ftp_rollback",
1219
- description: "Rollback a previous transaction using its snapshot",
1302
+ description: "Undo a previous file mutation by restoring it from a system-generated snapshot. This provides safety during complex refactoring tasks.",
1220
1303
  inputSchema: {
1221
1304
  type: "object",
1222
1305
  properties: {
1223
1306
  transactionId: {
1224
1307
  type: "string",
1225
- description: "Transaction ID to rollback (e.g., tx_1234567890_abcd)"
1308
+ description: "Specific ID associated with the snapshot (e.g., 'tx_12345'). Get this from ftp_list_transactions."
1226
1309
  }
1227
1310
  },
1228
1311
  required: ["transactionId"]
@@ -1230,7 +1313,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1230
1313
  },
1231
1314
  {
1232
1315
  name: "ftp_list_transactions",
1233
- description: "List available rollback transactions",
1316
+ description: "Expose all recent mutations currently stored in the system's SnapshotManager. Essential for planning rollbacks.",
1234
1317
  inputSchema: {
1235
1318
  type: "object",
1236
1319
  properties: {}
@@ -1238,13 +1321,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1238
1321
  },
1239
1322
  {
1240
1323
  name: "ftp_probe_capabilities",
1241
- description: "Probe the server to detect supported features (chmod, symlinks, disk space, etc.)",
1324
+ description: "Scan the remote server to determine supported filesystem features (e.g., chmod availability, symlink support, disk space querying). Helpful for troubleshooting capabilities.",
1242
1325
  inputSchema: {
1243
1326
  type: "object",
1244
1327
  properties: {
1245
1328
  testPath: {
1246
1329
  type: "string",
1247
- description: "A safe remote directory to run tests in (defaults to current directory)",
1330
+ description: "A safe, ephemeral directory to run capability benchmarks in.",
1248
1331
  default: "."
1249
1332
  }
1250
1333
  }
@@ -1252,7 +1335,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1252
1335
  },
1253
1336
  {
1254
1337
  name: "ftp_telemetry",
1255
- description: "Get connection health and performance telemetry",
1338
+ description: "Retrieve internal performance metrics, including average latency, connection pool health, and total processed bytes.",
1256
1339
  inputSchema: {
1257
1340
  type: "object",
1258
1341
  properties: {}
@@ -1262,6 +1345,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1262
1345
  };
1263
1346
  });
1264
1347
 
1348
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
1349
+ return {
1350
+ resources: [
1351
+ {
1352
+ uri: "mcp://instruction/server-state",
1353
+ name: "Active Server Instruction & Context",
1354
+ description: "Provides a real-time summary of the active connection profile, security constraints, and operational hints to optimize agent behavior.",
1355
+ mimeType: "text/markdown"
1356
+ }
1357
+ ]
1358
+ };
1359
+ });
1360
+
1361
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1362
+ if (request.params.uri === "mcp://instruction/server-state") {
1363
+ const isReadOnly = currentConfig?.readOnly || false;
1364
+ const protocol = currentConfig?.host?.startsWith('sftp://') ? 'SFTP' : 'FTP';
1365
+
1366
+ const content = `# ftp-mcp Server Context (Agent Guide)
1367
+ **Current Version:** ${SERVER_VERSION}
1368
+ **Active Profile:** ${currentProfile || 'Environment Variables'}
1369
+ **Connection Mode:** ${protocol}
1370
+ **Security Status:** ${isReadOnly ? 'READ-ONLY (Destructive operations disabled)' : 'READ-WRITE'}
1371
+
1372
+ ## 💡 Operational Recommendations:
1373
+ 1. **Prefer Patches**: Use \`ftp_patch_file\` instead of \`ftp_put_contents\` for existing files to minimize token usage and bandwidth.
1374
+ 2. **Batch for Speed**: Use \`ftp_batch_upload\` and \`ftp_batch_download\` for multi-file operations.
1375
+ 3. **Workspace Context**: If this is a new codebase, run \`ftp_analyze_workspace "."\` to identify framework patterns.
1376
+ 4. **Safety**: Server uses automatic SHA-256 drift protection in snapshots. Use \`ftp_rollback\` if a refactor goes wrong.
1377
+
1378
+ [END OF SYSTEM INSTRUCTION]`;
1379
+
1380
+ return {
1381
+ contents: [
1382
+ {
1383
+ uri: request.params.uri,
1384
+ mimeType: "text/markdown",
1385
+ text: content
1386
+ }
1387
+ ]
1388
+ };
1389
+ }
1390
+ throw new Error(`Resource not found: ${request.params.uri}`);
1391
+ });
1392
+
1265
1393
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1266
1394
  if (request.params.name === "ftp_list_deployments") {
1267
1395
  try {
@@ -1397,10 +1525,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1397
1525
  warning = "\n⚠️ SECURITY WARNING: You are connecting to a production profile using insecure FTP. SFTP is strongly recommended.";
1398
1526
  }
1399
1527
 
1528
+ let hint = "";
1529
+ if (!sessionHintShown) {
1530
+ hint = `\n\n${getAISuggestion('hint_connected')}`;
1531
+ sessionHintShown = true;
1532
+ }
1533
+
1400
1534
  return {
1401
1535
  content: [{
1402
1536
  type: "text",
1403
- text: `Connected to profile: ${profile || currentProfile || 'environment variables'}\nHost: ${currentConfig.host}${warning}`
1537
+ text: `Connected to profile: ${profile || currentProfile || 'environment variables'}\nHost: ${currentConfig.host}${warning}${hint}`
1404
1538
  }]
1405
1539
  };
1406
1540
  } catch (error) {
@@ -1479,14 +1613,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1479
1613
  const sliced = files.slice(offset, offset + limit);
1480
1614
 
1481
1615
  const formatted = sliced.map(f => {
1482
- const type = (useSFTP ? f.type === 'd' : f.isDirectory) ? 'DIR ' : 'FILE';
1616
+ const isDir = (useSFTP ? f.type === 'd' : f.isDirectory);
1617
+ const icon = isDir ? ICON.DIR : ICON.FILE;
1618
+ const label = isDir ? '[DIR] ' : '[FILE]';
1619
+
1620
+ let marker = "";
1621
+ const nameLower = f.name.toLowerCase();
1622
+ if (['package.json', 'composer.json', 'requirements.txt', 'pyproject.toml', 'go.mod'].includes(nameLower)) marker = ` ${ICON.PKG}`;
1623
+ else if (nameLower.includes('config') || nameLower.endsWith('.conf') || nameLower.endsWith('.yaml') || nameLower.endsWith('.yml')) marker = ` ${ICON.CONFIG}`;
1624
+ else if (isSecretFile(f.name)) marker = ` ${ICON.SECRET}`;
1625
+ else if (nameLower.endsWith('.bak') || nameLower.endsWith('.tmp') || nameLower.startsWith('~')) marker = ` ${ICON.BACKUP}`;
1626
+
1483
1627
  const rights = useSFTP && f.rights ? `, ${f.rights.user || ''}${f.rights.group || ''}${f.rights.other || ''}` : '';
1484
- return `${type} ${f.name} (${f.size} bytes${rights})`;
1628
+ return `${icon}${marker} ${label} ${f.name} (${f.size} bytes${rights})`;
1485
1629
  }).join('\n');
1486
1630
 
1487
1631
  const paginationInfo = `\n\nShowing ${offset + 1} to ${Math.min(offset + limit, total)} of ${total} items.`;
1632
+ const hint = total > 0 && sliced.some(f => f.name === 'package.json') ? `\n\n${getAISuggestion('hint_list_config')}` : "";
1633
+
1488
1634
  return {
1489
- content: [{ type: "text", text: (formatted || "Empty directory") + (total > limit ? paginationInfo : '') }]
1635
+ content: [{ type: "text", text: (formatted || "Empty directory") + (total > limit ? paginationInfo : '') + hint }]
1490
1636
  };
1491
1637
  }
1492
1638
 
@@ -1804,8 +1950,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1804
1950
  }
1805
1951
 
1806
1952
  const paginationInfo = `\n\nShowing ${offset + 1} to ${Math.min(offset + limit, total)} of ${total} matches.`;
1953
+ const hint = total === 0 ? `\n\n[AI: No matches found. Suggested fix: Try a broader wildcard pattern like \`*\` or verify your current \`path\` is correct.]` : "";
1807
1954
  return {
1808
- content: [{ type: "text", text: (formatted || "No matches found") + (total > limit ? paginationInfo : '') }]
1955
+ content: [{ type: "text", text: (formatted || "No matches found") + (total > limit ? paginationInfo : '') + hint }]
1809
1956
  };
1810
1957
  }
1811
1958
 
@@ -2361,8 +2508,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2361
2508
  return response;
2362
2509
  } catch (error) {
2363
2510
  console.error(`[Fatal Tool Error] ${request.params.name}:`, error);
2511
+ let suggestion = "";
2512
+ const nameLower = error.message.toLowerCase();
2513
+ if (nameLower.includes('enoent') || nameLower.includes('not found')) {
2514
+ suggestion = `\n\n${getAISuggestion('error_enoent', { path: request.params.arguments?.path || request.params.arguments?.remotePath || 'target' })}`;
2515
+ } else if (nameLower.includes('permission') || nameLower.includes('eacces')) {
2516
+ suggestion = `\n\n${getAISuggestion('error_permission', { path: request.params.arguments?.path || request.params.arguments?.remotePath || 'target' })}`;
2517
+ }
2518
+
2364
2519
  return {
2365
- content: [{ type: "text", text: `Error: ${error.message}` }],
2520
+ content: [{ type: "text", text: `Error: ${error.message}${suggestion}` }],
2366
2521
  isError: true
2367
2522
  };
2368
2523
  } finally {
package/package.json CHANGED
@@ -1,122 +1,122 @@
1
- {
2
- "name": "ftp-mcp",
3
- "version": "1.3.0",
4
- "description": "Enterprise-grade MCP server providing heavily optimized FTP/SFTP operations with smart sync, patch/chunk streaming, caching, and explicit read-only security mappings for AI code assistants.",
5
- "type": "module",
6
- "main": "index.js",
7
- "bin": {
8
- "ftp-mcp": "./index.js",
9
- "ftp-mcp-server": "./index.js"
10
- },
11
- "files": [
12
- "index.js",
13
- "policy-engine.js",
14
- "snapshot-manager.js",
15
- "sync-manifest.js",
16
- "README.md",
17
- "LICENSE",
18
- ".ftpconfig.example"
19
- ],
20
- "scripts": {
21
- "start": "node index.js",
22
- "test": "vitest run"
23
- },
24
- "keywords": [
25
- "mcp",
26
- "mcp-protocol",
27
- "model-context-protocol",
28
- "ftp",
29
- "ftp-client",
30
- "sftp",
31
- "sftp-client",
32
- "ssh",
33
- "ssh2",
34
- "scp",
35
- "ssh-agent",
36
- "rsync",
37
- "file-transfer",
38
- "file-manager",
39
- "sync",
40
- "diff",
41
- "patch",
42
- "git",
43
- "gitignore",
44
- "deploy",
45
- "deployment",
46
- "devops",
47
- "continuous-deployment",
48
- "automation",
49
- "ai",
50
- "ai-tools",
51
- "llm",
52
- "assistant",
53
- "agentic",
54
- "code-assistant",
55
- "tool-call",
56
- "json-rpc",
57
- "server",
58
- "tools",
59
- "claude",
60
- "claude-mcp",
61
- "claude-ftp",
62
- "cline",
63
- "cline-mcp",
64
- "cline-ftp",
65
- "cursor",
66
- "cursor-mcp",
67
- "cursor-ftp",
68
- "windsurf",
69
- "windsurf-mcp",
70
- "roo",
71
- "roo-code",
72
- "roo-mcp",
73
- "copilot",
74
- "github-copilot",
75
- "amp",
76
- "amp-mcp",
77
- "openai",
78
- "anthropic",
79
- "gemini",
80
- "gemini-mcp",
81
- "mcp-server",
82
- "mcp-tools",
83
- "server-tool",
84
- "remote",
85
- "remote-fs",
86
- "vfs"
87
- ],
88
- "engines": {
89
- "node": ">=18.0.0"
90
- },
91
- "author": {
92
- "name": "Kynlo Akari",
93
- "url": "https://github.com/Kynlos/"
94
- },
95
- "license": "MIT",
96
- "repository": {
97
- "type": "git",
98
- "url": "https://github.com/Kynlos/ftp-mcp.git"
99
- },
100
- "homepage": "https://github.com/Kynlos/ftp-mcp#readme",
101
- "bugs": {
102
- "url": "https://github.com/Kynlos/ftp-mcp/issues"
103
- },
104
- "publishConfig": {
105
- "access": "public"
106
- },
107
- "dependencies": {
108
- "@clack/prompts": "^1.2.0",
109
- "@modelcontextprotocol/sdk": "^1.0.4",
110
- "basic-ftp": "^5.0.5",
111
- "diff": "^8.0.4",
112
- "dotenv": "^17.4.0",
113
- "ignore": "^7.0.5",
114
- "minimatch": "^10.0.3",
115
- "ssh2-sftp-client": "^11.0.0",
116
- "zod": "^4.3.6"
117
- },
118
- "devDependencies": {
119
- "ftp-srv": "^4.6.3",
120
- "vitest": "^4.1.2"
121
- }
122
- }
1
+ {
2
+ "name": "ftp-mcp",
3
+ "version": "1.4.0",
4
+ "description": "Enterprise-grade MCP server providing heavily optimized FTP/SFTP operations with smart sync, patch/chunk streaming, caching, and explicit read-only security mappings for AI code assistants.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "ftp-mcp": "./index.js",
9
+ "ftp-mcp-server": "./index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "policy-engine.js",
14
+ "snapshot-manager.js",
15
+ "sync-manifest.js",
16
+ "README.md",
17
+ "LICENSE",
18
+ ".ftpconfig.example"
19
+ ],
20
+ "scripts": {
21
+ "start": "node index.js",
22
+ "test": "vitest run"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "mcp-protocol",
27
+ "model-context-protocol",
28
+ "ftp",
29
+ "ftp-client",
30
+ "sftp",
31
+ "sftp-client",
32
+ "ssh",
33
+ "ssh2",
34
+ "scp",
35
+ "ssh-agent",
36
+ "rsync",
37
+ "file-transfer",
38
+ "file-manager",
39
+ "sync",
40
+ "diff",
41
+ "patch",
42
+ "git",
43
+ "gitignore",
44
+ "deploy",
45
+ "deployment",
46
+ "devops",
47
+ "continuous-deployment",
48
+ "automation",
49
+ "ai",
50
+ "ai-tools",
51
+ "llm",
52
+ "assistant",
53
+ "agentic",
54
+ "code-assistant",
55
+ "tool-call",
56
+ "json-rpc",
57
+ "server",
58
+ "tools",
59
+ "claude",
60
+ "claude-mcp",
61
+ "claude-ftp",
62
+ "cline",
63
+ "cline-mcp",
64
+ "cline-ftp",
65
+ "cursor",
66
+ "cursor-mcp",
67
+ "cursor-ftp",
68
+ "windsurf",
69
+ "windsurf-mcp",
70
+ "roo",
71
+ "roo-code",
72
+ "roo-mcp",
73
+ "copilot",
74
+ "github-copilot",
75
+ "amp",
76
+ "amp-mcp",
77
+ "openai",
78
+ "anthropic",
79
+ "gemini",
80
+ "gemini-mcp",
81
+ "mcp-server",
82
+ "mcp-tools",
83
+ "server-tool",
84
+ "remote",
85
+ "remote-fs",
86
+ "vfs"
87
+ ],
88
+ "engines": {
89
+ "node": ">=18.0.0"
90
+ },
91
+ "author": {
92
+ "name": "Kynlo Akari",
93
+ "url": "https://github.com/Kynlos/"
94
+ },
95
+ "license": "MIT",
96
+ "repository": {
97
+ "type": "git",
98
+ "url": "https://github.com/Kynlos/ftp-mcp.git"
99
+ },
100
+ "homepage": "https://github.com/Kynlos/ftp-mcp#readme",
101
+ "bugs": {
102
+ "url": "https://github.com/Kynlos/ftp-mcp/issues"
103
+ },
104
+ "publishConfig": {
105
+ "access": "public"
106
+ },
107
+ "dependencies": {
108
+ "@clack/prompts": "^1.2.0",
109
+ "@modelcontextprotocol/sdk": "^1.0.4",
110
+ "basic-ftp": "^5.0.5",
111
+ "diff": "^8.0.4",
112
+ "dotenv": "^17.4.0",
113
+ "ignore": "^7.0.5",
114
+ "minimatch": "^10.0.3",
115
+ "ssh2-sftp-client": "^11.0.0",
116
+ "zod": "^4.3.6"
117
+ },
118
+ "devDependencies": {
119
+ "ftp-srv": "^4.6.3",
120
+ "vitest": "^4.1.2"
121
+ }
122
+ }