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.
- package/index.js +72 -6
- 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
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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.
|
|
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",
|