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.
- package/README.md +2 -1
- package/index.js +258 -103
- 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
|
-
- **
|
|
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.
|
|
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:
|
|
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:
|
|
54
|
-
|
|
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:
|
|
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:
|
|
65
|
-
|
|
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({
|
|
74
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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: "
|
|
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: "
|
|
846
|
+
description: "The named profile key from your .ftpconfig file."
|
|
763
847
|
},
|
|
764
848
|
useEnv: {
|
|
765
849
|
type: "boolean",
|
|
766
|
-
description: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
891
|
+
description: "Maximum results to return in this chunk.",
|
|
808
892
|
default: 100
|
|
809
893
|
},
|
|
810
894
|
offset: {
|
|
811
895
|
type: "number",
|
|
812
|
-
description: "
|
|
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
|
|
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: "
|
|
910
|
+
description: "Absolute or relative remote path to the file."
|
|
827
911
|
},
|
|
828
912
|
startLine: {
|
|
829
913
|
type: "number",
|
|
830
|
-
description: "Optional
|
|
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
|
|
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
|
|
932
|
+
description: "Remote path to the existing file to be patched."
|
|
849
933
|
},
|
|
850
934
|
patch: {
|
|
851
935
|
type: "string",
|
|
852
|
-
description: "Unified
|
|
936
|
+
description: "The Unified Diff formatted string containing your local changes."
|
|
853
937
|
},
|
|
854
938
|
expectedHash: {
|
|
855
939
|
type: "string",
|
|
856
|
-
description: "Optional
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
973
|
+
description: "Remote destination path."
|
|
890
974
|
},
|
|
891
975
|
content: {
|
|
892
976
|
type: "string",
|
|
893
|
-
description: "
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
1046
|
+
description: "Restrict search to specific file extensions (e.g. `.css`)."
|
|
963
1047
|
},
|
|
964
1048
|
findLikelyConfigs: {
|
|
965
1049
|
type: "boolean",
|
|
966
|
-
description: "
|
|
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
|
|
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
|
|
1060
|
+
description: "Maximum matches to return.",
|
|
977
1061
|
default: 50
|
|
978
1062
|
},
|
|
979
1063
|
offset: {
|
|
980
1064
|
type: "number",
|
|
981
|
-
description: "
|
|
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: "
|
|
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: "
|
|
1079
|
+
description: "Qualified remote source path."
|
|
996
1080
|
},
|
|
997
1081
|
destPath: {
|
|
998
1082
|
type: "string",
|
|
999
|
-
description: "
|
|
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
|
|
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: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1141
|
+
description: "The source directory on your local machine."
|
|
1058
1142
|
},
|
|
1059
1143
|
remotePath: {
|
|
1060
1144
|
type: "string",
|
|
1061
|
-
description: "
|
|
1145
|
+
description: "The target destination directory on the remote server."
|
|
1062
1146
|
},
|
|
1063
1147
|
direction: {
|
|
1064
1148
|
type: "string",
|
|
1065
|
-
|
|
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,
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1189
|
+
description: "Source path on your local machine."
|
|
1107
1190
|
},
|
|
1108
1191
|
remotePath: {
|
|
1109
1192
|
type: "string",
|
|
1110
|
-
description: "
|
|
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: "
|
|
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: "
|
|
1207
|
+
description: "Source file location on the remote server."
|
|
1125
1208
|
},
|
|
1126
1209
|
localPath: {
|
|
1127
1210
|
type: "string",
|
|
1128
|
-
description: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
1253
|
+
description: "The remote directory path to remove."
|
|
1171
1254
|
},
|
|
1172
1255
|
recursive: {
|
|
1173
1256
|
type: "boolean",
|
|
1174
|
-
description: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
|
1290
|
+
description: "Current remote location."
|
|
1208
1291
|
},
|
|
1209
1292
|
newPath: {
|
|
1210
1293
|
type: "string",
|
|
1211
|
-
description: "New
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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 `${
|
|
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.
|
|
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
|
+
}
|