ftp-mcp 1.3.1 ā 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/index.js +117 -8
- package/package.json +1 -1
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;
|
|
@@ -151,6 +186,7 @@ if (process.argv.includes("--init")) {
|
|
|
151
186
|
|
|
152
187
|
let currentConfig = null;
|
|
153
188
|
let currentProfile = null;
|
|
189
|
+
let sessionHintShown = false;
|
|
154
190
|
|
|
155
191
|
const DEFAULT_IGNORE_PATTERNS = [
|
|
156
192
|
'node_modules/**',
|
|
@@ -786,11 +822,12 @@ function generateSemanticPreview(filesToChange) {
|
|
|
786
822
|
const server = new Server(
|
|
787
823
|
{
|
|
788
824
|
name: "ftp-mcp-server",
|
|
789
|
-
version: SERVER_VERSION,
|
|
825
|
+
version: SERVER_VERSION,
|
|
790
826
|
},
|
|
791
827
|
{
|
|
792
828
|
capabilities: {
|
|
793
829
|
tools: {},
|
|
830
|
+
resources: {},
|
|
794
831
|
},
|
|
795
832
|
}
|
|
796
833
|
);
|
|
@@ -1308,6 +1345,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1308
1345
|
};
|
|
1309
1346
|
});
|
|
1310
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
|
+
|
|
1311
1393
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1312
1394
|
if (request.params.name === "ftp_list_deployments") {
|
|
1313
1395
|
try {
|
|
@@ -1443,10 +1525,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1443
1525
|
warning = "\nā ļø SECURITY WARNING: You are connecting to a production profile using insecure FTP. SFTP is strongly recommended.";
|
|
1444
1526
|
}
|
|
1445
1527
|
|
|
1528
|
+
let hint = "";
|
|
1529
|
+
if (!sessionHintShown) {
|
|
1530
|
+
hint = `\n\n${getAISuggestion('hint_connected')}`;
|
|
1531
|
+
sessionHintShown = true;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1446
1534
|
return {
|
|
1447
1535
|
content: [{
|
|
1448
1536
|
type: "text",
|
|
1449
|
-
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}`
|
|
1450
1538
|
}]
|
|
1451
1539
|
};
|
|
1452
1540
|
} catch (error) {
|
|
@@ -1525,14 +1613,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1525
1613
|
const sliced = files.slice(offset, offset + limit);
|
|
1526
1614
|
|
|
1527
1615
|
const formatted = sliced.map(f => {
|
|
1528
|
-
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
|
+
|
|
1529
1627
|
const rights = useSFTP && f.rights ? `, ${f.rights.user || ''}${f.rights.group || ''}${f.rights.other || ''}` : '';
|
|
1530
|
-
return `${
|
|
1628
|
+
return `${icon}${marker} ${label} ${f.name} (${f.size} bytes${rights})`;
|
|
1531
1629
|
}).join('\n');
|
|
1532
1630
|
|
|
1533
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
|
+
|
|
1534
1634
|
return {
|
|
1535
|
-
content: [{ type: "text", text: (formatted || "Empty directory") + (total > limit ? paginationInfo : '') }]
|
|
1635
|
+
content: [{ type: "text", text: (formatted || "Empty directory") + (total > limit ? paginationInfo : '') + hint }]
|
|
1536
1636
|
};
|
|
1537
1637
|
}
|
|
1538
1638
|
|
|
@@ -1850,8 +1950,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1850
1950
|
}
|
|
1851
1951
|
|
|
1852
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.]` : "";
|
|
1853
1954
|
return {
|
|
1854
|
-
content: [{ type: "text", text: (formatted || "No matches found") + (total > limit ? paginationInfo : '') }]
|
|
1955
|
+
content: [{ type: "text", text: (formatted || "No matches found") + (total > limit ? paginationInfo : '') + hint }]
|
|
1855
1956
|
};
|
|
1856
1957
|
}
|
|
1857
1958
|
|
|
@@ -2407,8 +2508,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2407
2508
|
return response;
|
|
2408
2509
|
} catch (error) {
|
|
2409
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
|
+
|
|
2410
2519
|
return {
|
|
2411
|
-
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
2520
|
+
content: [{ type: "text", text: `Error: ${error.message}${suggestion}` }],
|
|
2412
2521
|
isError: true
|
|
2413
2522
|
};
|
|
2414
2523
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ftp-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|