ftp-mcp 1.2.0 → 1.2.2

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 CHANGED
@@ -86,7 +86,7 @@ export FTPMCP_AGENT="pageant"
86
86
 
87
87
  ### 3. Client Integration
88
88
 
89
- Register the MCP server directly to your AI Code Editor (Cursor, VSCode with Roo, Windsurf, or Amplitude).
89
+ Register the MCP server directly to your AI Code Editor (Cursor, Cline, Claude Desktop, Windsurf, etc.).
90
90
 
91
91
  Example integration:
92
92
  - **Command:** `npx`
@@ -151,6 +151,10 @@ Deploy environments utilizing deep comparison trees via `ftp_sync`.
151
151
  - Audit Logging is continuously enabled. Ensure the terminal running the MCP protocol maintains structural write-permissions to the local project deployment folder.
152
152
  - Ensure any `currentConfig` caching resets are tied exclusively to `ftp_connect` profile pivoting.
153
153
 
154
+ ## Changelog
155
+
156
+ See [CHANGELOG.md](CHANGELOG.md) for a detailed list of changes in each release.
157
+
154
158
  ## License
155
159
 
156
160
  [MIT](LICENSE)
package/index.js CHANGED
@@ -27,7 +27,7 @@ const CONFIG_FILE = process.env.FTP_CONFIG_PATH || path.join(process.cwd(), DEFA
27
27
  // --init: scaffold .ftpconfig.example into the user's current working directory
28
28
  if (process.argv.includes("--init")) {
29
29
  try {
30
- const { intro, outro, text, password: promptPassword, select, confirm, note } = await import("@clack/prompts");
30
+ const { intro, outro, text, password: promptPassword, select, confirm, note, isCancel } = await import("@clack/prompts");
31
31
 
32
32
  intro('🚀 Welcome to FTP-MCP Initialization Wizard');
33
33
 
@@ -36,30 +36,35 @@ if (process.argv.includes("--init")) {
36
36
  placeholder: 'sftp://127.0.0.1',
37
37
  validate: (val) => val.length === 0 ? "Host is required!" : undefined,
38
38
  });
39
+ if (isCancel(host)) { outro('Setup cancelled.'); process.exit(0); }
39
40
 
40
41
  const user = await text({
41
42
  message: 'Enter your Username',
42
43
  validate: (val) => val.length === 0 ? "User is required!" : undefined,
43
44
  });
45
+ if (isCancel(user)) { outro('Setup cancelled.'); process.exit(0); }
44
46
 
45
47
  const pass = await promptPassword({
46
48
  message: 'Enter your Password (optional if using keys)',
47
49
  });
50
+ if (isCancel(pass)) { outro('Setup cancelled.'); process.exit(0); }
48
51
 
49
52
  const port = await text({
50
53
  message: 'Enter port (optional, defaults to 21 for FTP, 22 for SFTP)',
51
54
  placeholder: '22'
52
55
  });
56
+ if (isCancel(port)) { outro('Setup cancelled.'); process.exit(0); }
53
57
 
54
- const isSFTP = host.startsWith('sftp://');
58
+ const isSFTP = typeof host === 'string' && host.startsWith('sftp://');
55
59
  let privateKey = '';
56
60
 
57
61
  if (isSFTP) {
58
62
  const usesKey = await confirm({ message: 'Are you using an SSH Private Key instead of a password?' });
59
- if (usesKey) {
63
+ if (!isCancel(usesKey) && usesKey) {
60
64
  privateKey = await text({
61
65
  message: 'Path to your private key (e.g. ~/.ssh/id_rsa)',
62
66
  });
67
+ if (isCancel(privateKey)) { outro('Setup cancelled.'); process.exit(0); }
63
68
  }
64
69
  }
65
70
 
@@ -450,7 +455,7 @@ function releaseClient(config) {
450
455
  async function getTreeRecursive(client, useSFTP, remotePath, depth = 0, maxDepth = 10) {
451
456
  if (depth > maxDepth) return [];
452
457
 
453
- const files = useSFTP ? await client.list(remotePath) : await client.list(remotePath);
458
+ const files = await client.list(remotePath);
454
459
  const results = [];
455
460
 
456
461
  for (const file of files) {
@@ -475,12 +480,13 @@ async function getTreeRecursive(client, useSFTP, remotePath, depth = 0, maxDepth
475
480
  return results;
476
481
  }
477
482
 
478
- async function syncFiles(client, useSFTP, localPath, remotePath, direction, ignorePatterns = null, basePath = null, extraExclude = [], dryRun = false, useManifest = true) {
483
+ async function syncFiles(client, useSFTP, localPath, remotePath, direction, ignorePatterns = null, basePath = null, extraExclude = [], dryRun = false, useManifest = true, _isTopLevel = false) {
479
484
  const stats = { uploaded: 0, downloaded: 0, skipped: 0, errors: [], ignored: 0, filesToChange: [] };
480
485
 
481
486
  if (ignorePatterns === null) {
482
487
  ignorePatterns = await loadIgnorePatterns(localPath);
483
488
  basePath = localPath;
489
+ _isTopLevel = true;
484
490
  if (useManifest) await syncManifestManager.load();
485
491
  }
486
492
 
@@ -496,8 +502,8 @@ async function syncFiles(client, useSFTP, localPath, remotePath, direction, igno
496
502
  const remoteFilePath = `${remotePath}/${file.name}`;
497
503
 
498
504
  // In some environments (like Windows with ftp-srv), rapid transfers cause ECONNRESET.
499
- // A slightly longer delay helps stabilize the socket state during sequence.
500
- await new Promise(r => setTimeout(r, 250));
505
+ // A short delay helps stabilize the socket state during sequence (FTP only).
506
+ if (!useSFTP) await new Promise(r => setTimeout(r, 50));
501
507
 
502
508
  // Security check first so we can warn even if it's in .gitignore/.ftpignore
503
509
  if (isSecretFile(localFilePath)) {
@@ -602,7 +608,7 @@ async function syncFiles(client, useSFTP, localPath, remotePath, direction, igno
602
608
  }
603
609
  }
604
610
 
605
- if (ignorePatterns === null && useManifest && !dryRun) {
611
+ if (_isTopLevel && useManifest && !dryRun) {
606
612
  await syncManifestManager.save();
607
613
  }
608
614
 
@@ -657,7 +663,7 @@ function generateSemanticPreview(filesToChange) {
657
663
  const server = new Server(
658
664
  {
659
665
  name: "ftp-mcp-server",
660
- version: "1.2.0",
666
+ version: "1.2.1",
661
667
  },
662
668
  {
663
669
  capabilities: {
@@ -1182,8 +1188,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1182
1188
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1183
1189
  if (request.params.name === "ftp_list_deployments") {
1184
1190
  try {
1185
- const configPath = path.join(process.cwd(), '.ftpconfig');
1186
- const configData = await fs.readFile(configPath, 'utf8');
1191
+ const configData = await fs.readFile(CONFIG_FILE, 'utf8');
1187
1192
  const config = JSON.parse(configData);
1188
1193
 
1189
1194
  if (!config.deployments || Object.keys(config.deployments).length === 0) {
@@ -1216,8 +1221,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1216
1221
  if (request.params.name === "ftp_deploy") {
1217
1222
  try {
1218
1223
  const { deployment } = request.params.arguments;
1219
- const configPath = path.join(process.cwd(), '.ftpconfig');
1220
- const configData = await fs.readFile(configPath, 'utf8');
1224
+ const configData = await fs.readFile(CONFIG_FILE, 'utf8');
1221
1225
  const config = JSON.parse(configData);
1222
1226
 
1223
1227
  if (!config.deployments || !config.deployments[deployment]) {
@@ -1376,7 +1380,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1376
1380
 
1377
1381
  let files = getCached(poolKey, 'LIST', path);
1378
1382
  if (!files) {
1379
- files = useSFTP ? await client.list(path) : await client.list(path);
1383
+ files = await client.list(path);
1380
1384
  files.sort((a, b) => a.name.localeCompare(b.name));
1381
1385
  setCached(poolKey, 'LIST', path, files);
1382
1386
  }
@@ -1655,7 +1659,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1655
1659
  let formatted = "";
1656
1660
 
1657
1661
  if (contentPattern) {
1658
- const contentRegex = new RegExp(contentPattern, 'gi');
1662
+ const contentRegex = new RegExp(contentPattern, 'i');
1659
1663
  const contentMatches = [];
1660
1664
 
1661
1665
  for (const item of sliced) {
@@ -1703,7 +1707,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1703
1707
  const path = request.params.arguments?.path || ".";
1704
1708
  let files = getCached(poolKey, 'LIST', path);
1705
1709
  if (!files) {
1706
- files = useSFTP ? await client.list(path) : await client.list(path);
1710
+ files = await client.list(path);
1707
1711
  setCached(poolKey, 'LIST', path, files);
1708
1712
  }
1709
1713
 
@@ -2008,7 +2012,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2008
2012
  if (recursive) {
2009
2013
  await client.removeDir(path);
2010
2014
  } else {
2011
- await client.remove(path);
2015
+ await client.send("RMD", path);
2012
2016
  }
2013
2017
  }
2014
2018
 
@@ -2045,11 +2049,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2045
2049
 
2046
2050
  const txId = await snapshotManager.createSnapshot(client, useSFTP, [oldPath, newPath]);
2047
2051
 
2048
- if (useSFTP) {
2049
- await client.rename(oldPath, newPath);
2050
- } else {
2051
- await client.rename(oldPath, newPath);
2052
- }
2052
+ await client.rename(oldPath, newPath);
2053
2053
 
2054
2054
  return {
2055
2055
  content: [{ type: "text", text: `Successfully renamed ${oldPath} to ${newPath}\nTransaction ID: ${txId}` }]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftp-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
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
5
  "type": "module",
6
6
  "main": "index.js",
package/policy-engine.js CHANGED
@@ -10,7 +10,12 @@ export class PolicyEngine {
10
10
  if (!this.policies.allowedPaths || this.policies.allowedPaths.length === 0) {
11
11
  return true; // No restriction
12
12
  }
13
- return this.policies.allowedPaths.some(allowed => filePath.startsWith(allowed));
13
+ return this.policies.allowedPaths.some(allowed => {
14
+ // Normalize: ensure trailing slash to prevent /var/www matching /var/www-evil
15
+ const normalizedAllowed = allowed.endsWith('/') ? allowed : allowed + '/';
16
+ const normalizedPath = filePath.endsWith('/') ? filePath : filePath + '/';
17
+ return normalizedPath.startsWith(normalizedAllowed) || filePath === allowed;
18
+ });
14
19
  }
15
20
 
16
21
  checkBlockedGlob(filePath) {