ftp-mcp 1.5.1 → 1.5.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.
Files changed (2) hide show
  1. package/index.js +72 -6
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -333,6 +333,9 @@ const ProfileConfigSchema = z.object({
333
333
  password: z.string().optional(),
334
334
  port: z.union([z.string(), z.number()]).optional(),
335
335
  secure: z.union([z.boolean(), z.literal("implicit")]).optional(),
336
+ secureOptions: z.record(z.any()).optional(),
337
+ verbose: z.boolean().optional(),
338
+ timeout: z.number().optional(),
336
339
  readOnly: z.boolean().optional(),
337
340
  privateKey: z.string().optional(),
338
341
  passphrase: z.string().optional(),
@@ -396,8 +399,9 @@ async function loadFTPConfig(profileName = null, forceEnv = false) {
396
399
 
397
400
  function getPort(host, configPort) {
398
401
  if (configPort) return parseInt(configPort, 10);
399
- // LOW-4: Use strict prefix check, not includes() which matches e.g. "mysftp-server.com"
402
+ // LOW-4: Use strict prefix check
400
403
  if (host && host.startsWith('sftp://')) return 22;
404
+ if (host && host.startsWith('ftp://')) return 21;
401
405
  return 21;
402
406
  }
403
407
 
@@ -409,13 +413,32 @@ function isSFTP(host) {
409
413
  async function connectFTP(config) {
410
414
  const client = new FTPClient();
411
415
  client.ftp.verbose = false;
416
+
417
+ if (config.verbose) {
418
+ client.ftp.log = (msg) => {
419
+ server.sendLoggingMessage({
420
+ level: "debug",
421
+ data: { message: `[FTP-RAW] ${msg}` }
422
+ });
423
+ };
424
+ }
425
+
412
426
  await client.access({
413
- host: config.host,
427
+ host: config.host.replace('ftp://', '').replace('sftp://', ''), // Strip protocol for basic-ftp
414
428
  user: config.user,
415
429
  password: config.password,
416
430
  port: getPort(config.host, config.port),
417
- secure: config.secure || false
431
+ secure: config.secure || false,
432
+ secureOptions: config.secureOptions || { rejectUnauthorized: false }, // Default to false for easy dev if not specified
433
+ timeout: config.timeout || 30000 // 30s connection timeout
418
434
  });
435
+
436
+ try {
437
+ client._initialPwd = await client.pwd();
438
+ } catch (e) {
439
+ client._initialPwd = '/'; // fallback if pwd fails
440
+ }
441
+
419
442
  return client;
420
443
  }
421
444
 
@@ -657,6 +680,31 @@ async function syncFiles(client, useSFTP, localPath, remotePath, direction, igno
657
680
  ignorePatterns = await loadIgnorePatterns(localPath);
658
681
  basePath = localPath;
659
682
  _isTopLevel = true;
683
+
684
+ // --- ANTI-RECURSION GUARD ---
685
+ // If we're syncing the current directory (.), and the remote target is a local folder (e.g. 'FlowdexMCP_LiveTest'),
686
+ // we MUST ignore the remote target folder locally so we don't upload it into itself forever.
687
+ const resolvedLocal = path.resolve(localPath);
688
+ // Rough check: if remotePath is just a folder name (no slashes) or a relative path, ignore it
689
+ if (remotePath && typeof remotePath === 'string' && !remotePath.startsWith('/')) {
690
+ const firstFolder = remotePath.split('/')[0];
691
+ if (firstFolder && firstFolder !== '.') {
692
+ ignorePatterns.push(firstFolder);
693
+ ignorePatterns.push(`${firstFolder}/**`);
694
+
695
+ // Re-instantiate the Ignore instance since we appended pattern strings
696
+ if (ignorePatterns._ig) delete ignorePatterns._ig;
697
+
698
+ server.notification({
699
+ method: "notifications/message",
700
+ params: {
701
+ level: "info",
702
+ data: { message: `[SYNC GUARD] Excluded target '${firstFolder}' from local sync to prevent infinite recursion.` }
703
+ }
704
+ });
705
+ }
706
+ }
707
+
660
708
  if (useManifest) await syncManifestManager.load();
661
709
 
662
710
  // Initialize progress tracking if a token is provided
@@ -675,6 +723,7 @@ async function syncFiles(client, useSFTP, localPath, remotePath, direction, igno
675
723
 
676
724
  if (extraExclude.length > 0) {
677
725
  ignorePatterns = [...ignorePatterns, ...extraExclude];
726
+ if (ignorePatterns._ig) delete ignorePatterns._ig;
678
727
  }
679
728
 
680
729
  if (direction === 'upload' || direction === 'both') {
@@ -704,6 +753,7 @@ async function syncFiles(client, useSFTP, localPath, remotePath, direction, igno
704
753
  await client.mkdir(remoteFilePath, true);
705
754
  } else {
706
755
  await client.ensureDir(remoteFilePath);
756
+ if (client._initialPwd) await client.cd(client._initialPwd).catch(() => {});
707
757
  }
708
758
  }
709
759
  const subStats = await syncFiles(client, useSFTP, localFilePath, remoteFilePath, direction, ignorePatterns, basePath, extraExclude, dryRun, useManifest, false, progressState);
@@ -726,9 +776,18 @@ async function syncFiles(client, useSFTP, localPath, remotePath, direction, igno
726
776
 
727
777
  if (shouldUpload) {
728
778
  try {
729
- const remoteStat = useSFTP
730
- ? await client.stat(remoteFilePath)
731
- : (await client.list(remotePath)).find(f => f.name === file.name);
779
+ let remoteStat = undefined;
780
+ if (useSFTP) {
781
+ remoteStat = await client.stat(remoteFilePath).catch(() => undefined);
782
+ } else {
783
+ // To avoid 1,000 MLSD calls, we fetch the directory list once if not cached
784
+ let dirList = getCached(`sync-${client.ftp?.host || 'local'}`, 'LIST', remotePath);
785
+ if (!dirList) {
786
+ dirList = await client.list(remotePath).catch(() => []);
787
+ setCached(`sync-${client.ftp?.host || 'local'}`, 'LIST', remotePath, dirList);
788
+ }
789
+ remoteStat = dirList.find(f => f.name === file.name);
790
+ }
732
791
 
733
792
  if (remoteStat) {
734
793
  const remoteTime = remoteStat.modifyTime || remoteStat.modifiedAt || new Date(remoteStat.rawModifiedAt);
@@ -2485,8 +2544,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2485
2544
  const txIdUpload = await snapshotManager.createSnapshot(client, useSFTP, [remotePath]);
2486
2545
 
2487
2546
  if (useSFTP) {
2547
+ await client.mkdir(path.dirname(remotePath), true).catch(() => {});
2488
2548
  await client.put(localPath, remotePath);
2489
2549
  } else {
2550
+ const dirName = remotePath.split('/').slice(0, -1).join('/');
2551
+ if (dirName && dirName !== '.') {
2552
+ await client.ensureDir(dirName).catch(() => {});
2553
+ await client.cd(client._initialPwd || '/').catch(() => {}); // reset to root
2554
+ }
2490
2555
  await client.uploadFrom(localPath, remotePath);
2491
2556
  }
2492
2557
 
@@ -2548,6 +2613,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2548
2613
  await client.mkdir(path, true);
2549
2614
  } else {
2550
2615
  await client.ensureDir(path);
2616
+ if (client._initialPwd) await client.cd(client._initialPwd).catch(() => {});
2551
2617
  }
2552
2618
 
2553
2619
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftp-mcp",
3
- "version": "1.5.1",
3
+ "version": "1.5.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",