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.
Files changed (2) hide show
  1. package/index.js +117 -8
  2. 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.3.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, // CODE-1: reads from package.json at startup
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 type = (useSFTP ? f.type === 'd' : f.isDirectory) ? 'DIR ' : 'FILE';
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 `${type} ${f.name} (${f.size} bytes${rights})`;
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.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",