#
tokens: 5355/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── build-windows.bat
├── Dockerfile
├── LICENSE
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── ftp-client.ts
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Dependency directories
node_modules/
jspm_packages/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Build output
build/
dist/
out/

# dotenv environment variable files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# IDEs and editors
.idea/
.vscode/
*.swp
*.swo
.DS_Store
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/alxspiker-mcp-server-ftp-badge.png)](https://mseep.ai/app/alxspiker-mcp-server-ftp)

# MCP Server for FTP Access

[![smithery badge](https://smithery.ai/badge/@alxspiker/mcp-server-ftp)](https://smithery.ai/server/@alxspiker/mcp-server-ftp)

This Model Context Protocol (MCP) server provides tools for interacting with FTP servers. It allows Claude.app to list directories, download and upload files, create directories, and delete files/directories on FTP servers.

## Features

- **List Directory Contents**: View files and folders on the FTP server
- **Download Files**: Retrieve file content from the FTP server
- **Upload Files**: Create new files or update existing ones
- **Create Directories**: Make new folders on the FTP server
- **Delete Files/Directories**: Remove files or directories

## Installation

### Installing via Smithery

To install mcp-server-ftp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@alxspiker/mcp-server-ftp):

```bash
npx -y @smithery/cli install @alxspiker/mcp-server-ftp --client claude
```

### Prerequisites

- Node.js 16 or higher
- Claude for Desktop (or other MCP-compatible client)

### Building from Source

#### Linux/macOS
```bash
# Clone the repository
git clone https://github.com/alxspiker/mcp-server-ftp.git
cd mcp-server-ftp

# Install dependencies
npm install

# Build the project
npm run build
```

#### Windows
```bash
# Clone the repository
git clone https://github.com/alxspiker/mcp-server-ftp.git
cd mcp-server-ftp

# Run the Windows build helper script
build-windows.bat
```

The `build-windows.bat` script handles dependency installation and building on Windows systems, with fallback options if the TypeScript compiler has issues.

## Configuration

To use this server with Claude for Desktop, add it to your configuration file:

### MacOS/Linux
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "ftp-server": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-server-ftp/build/index.js"],
      "env": {
        "FTP_HOST": "ftp.example.com",
        "FTP_PORT": "21",
        "FTP_USER": "your-username",
        "FTP_PASSWORD": "your-password",
        "FTP_SECURE": "false"
      }
    }
  }
}
```

### Windows
Edit `%APPDATA%\Claude\claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "ftp-server": {
      "command": "node",
      "args": ["C:\\path\\to\\mcp-server-ftp\\build\\index.js"],
      "env": {
        "FTP_HOST": "ftp.example.com",
        "FTP_PORT": "21",
        "FTP_USER": "your-username",
        "FTP_PASSWORD": "your-password",
        "FTP_SECURE": "false"
      }
    }
  }
}
```

## Troubleshooting Windows Build Issues

If you encounter build issues on Windows:

1. Use the provided `build-windows.bat` script which handles common build issues
2. Make sure Node.js and npm are properly installed
3. Try running the TypeScript compiler directly: `npx tsc`
4. If you still have issues, you can use the pre-compiled files in the `build` directory by running:
   ```
   node path\to\mcp-server-ftp\build\index.js
   ```

## Configuration Options

| Environment Variable | Description | Default |
|---------------------|-------------|---------|
| `FTP_HOST` | FTP server hostname or IP address | localhost |
| `FTP_PORT` | FTP server port | 21 |
| `FTP_USER` | FTP username | anonymous |
| `FTP_PASSWORD` | FTP password | (empty string) |
| `FTP_SECURE` | Use secure FTP (FTPS) | false |

## Usage

After configuring and restarting Claude for Desktop, you can use natural language to perform FTP operations:

- "List the files in the /public directory on my FTP server"
- "Download the file /data/report.txt from the FTP server"
- "Upload this text as a file called notes.txt to the FTP server"
- "Create a new directory called 'backups' on the FTP server"
- "Delete the file obsolete.txt from the FTP server"
- "Remove the empty directory /old-project from the FTP server"

## Available Tools

| Tool Name | Description |
|-----------|-------------|
| `list-directory` | List contents of an FTP directory |
| `download-file` | Download a file from the FTP server |
| `upload-file` | Upload a file to the FTP server |
| `create-directory` | Create a new directory on the FTP server |
| `delete-file` | Delete a file from the FTP server |
| `delete-directory` | Delete a directory from the FTP server |

## Security Considerations

- FTP credentials are stored in the Claude configuration file. Ensure this file has appropriate permissions.
- Consider using FTPS (secure FTP) by setting `FTP_SECURE=true` if your server supports it.
- The server creates temporary files for uploads and downloads in your system's temp directory.

## License

MIT

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

WORKDIR /app

# Copy necessary files
COPY package.json package-lock.json* ./
COPY tsconfig.json ./
COPY src ./src

# Install dependencies (ignoring scripts if necessary)
RUN npm install --ignore-scripts

# Build the project
RUN npm run build

# Expose any port if needed (not needed since it's a stdio server)

# Start the MCP server
CMD ["npm", "start"]

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "mcp-server-ftp",
  "version": "1.0.0",
  "description": "Model Context Protocol server for FTP access",
  "main": "build/index.js",
  "type": "module",
  "bin": {
    "mcp-server-ftp": "./build/index.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js",
    "dev": "npm run build && npm start",
    "prepublishOnly": "npm run build"
  },
  "keywords": [
    "mcp",
    "ftp",
    "cli",
    "model-context-protocol",
    "claude"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "basic-ftp": "^5.0.3",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.9.0",
    "typescript": "^5.2.2"
  },
  "files": [
    "build",
    "README.md",
    "LICENSE"
  ],
  "engines": {
    "node": ">=16.0.0"
  }
}
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - ftpHost
      - ftpPort
      - ftpUser
      - ftpPassword
      - ftpSecure
    properties:
      ftpHost:
        type: string
        default: localhost
        description: FTP server hostname or IP address
      ftpPort:
        type: number
        default: 21
        description: FTP server port
      ftpUser:
        type: string
        default: anonymous
        description: FTP username
      ftpPassword:
        type: string
        default: ""
        description: FTP password
      ftpSecure:
        type: boolean
        default: false
        description: Use secure FTP (FTPS)
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({
      command: 'node',
      args: ['build/index.js'],
      env: {
        FTP_HOST: config.ftpHost,
        FTP_PORT: String(config.ftpPort),
        FTP_USER: config.ftpUser,
        FTP_PASSWORD: config.ftpPassword,
        FTP_SECURE: String(config.ftpSecure)
      }
    })
  exampleConfig:
    ftpHost: ftp.example.com
    ftpPort: 21
    ftpUser: your-username
    ftpPassword: your-password
    ftpSecure: false

```

--------------------------------------------------------------------------------
/build-windows.bat:
--------------------------------------------------------------------------------

```
@echo off
echo Building mcp-server-ftp for Windows...

:: Check if Node.js is installed
where node >nul 2>nul
if %ERRORLEVEL% neq 0 (
    echo Node.js is not installed or not in your PATH.
    echo Please install Node.js from https://nodejs.org/
    exit /b 1
)

:: Check npm version
echo Checking npm version...
call npm -v >nul 2>nul
if %ERRORLEVEL% neq 0 (
    echo npm is not installed or not working correctly.
    exit /b 1
)

:: Install dependencies
echo Installing dependencies...
call npm install
if %ERRORLEVEL% neq 0 (
    echo Failed to install dependencies.
    exit /b 1
)

:: Check if TypeScript is installed
echo Checking TypeScript installation...
call npx tsc --version >nul 2>nul
if %ERRORLEVEL% neq 0 (
    echo TypeScript compiler not found. Installing...
    call npm install -g typescript
    if %ERRORLEVEL% neq 0 (
        echo Failed to install TypeScript.
        exit /b 1
    )
)

:: Create build directory if it doesn't exist
if not exist "build" mkdir build

:: Compile TypeScript
echo Compiling TypeScript files...
call npx tsc
if %ERRORLEVEL% neq 0 (
    echo TypeScript compilation failed.
    echo Falling back to manual file copy...

    :: Check if source files exist
    if not exist "src\index.ts" (
        echo Source files not found.
        exit /b 1
    )

    :: Copy TypeScript files to JavaScript files
    echo Copying TypeScript files to JavaScript...
    copy src\*.ts build\*.js
)

:: Check if build succeeded
if exist "build\index.js" (
    echo Build completed successfully!
    echo You can now run the server with:
    echo node build\index.js
) else (
    echo Build failed. Please check for errors.
    exit /b 1
)

echo.
echo Configuration instructions:
echo 1. Edit %APPDATA%\Claude\claude_desktop_config.json
echo 2. Add the FTP server configuration 
echo 3. Restart Claude Desktop
echo.
echo Refer to README.md for detailed instructions.

exit /b 0
```

--------------------------------------------------------------------------------
/src/ftp-client.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from "basic-ftp";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";

// Define FTP config interface
export interface FtpConfig {
  host: string;
  port: number;
  user: string;
  password: string;
  secure: boolean;
}

// Create FTP client wrapper
export class FtpClient {
  private client: Client;
  private config: FtpConfig;
  private tempDir: string;

  constructor(config: FtpConfig) {
    this.client = new Client();
    this.config = config;
    this.tempDir = path.join(os.tmpdir(), "mcp-ftp-temp");
    
    // Create temp directory if it doesn't exist
    if (!fs.existsSync(this.tempDir)) {
      fs.mkdirSync(this.tempDir, { recursive: true });
    }
    
    // Set client options
    this.client.ftp.verbose = false; // Set to true for debugging
  }

  async connect(): Promise<void> {
    try {
      await this.client.access({
        host: this.config.host,
        port: this.config.port,
        user: this.config.user,
        password: this.config.password,
        secure: this.config.secure
      });
    } catch (error) {
      console.error("FTP connection error:", error);
      throw new Error(`Failed to connect to FTP server: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  async disconnect(): Promise<void> {
    this.client.close();
  }

  async listDirectory(remotePath: string): Promise<Array<{name: string, type: string, size: number, modifiedDate: string}>> {
    try {
      await this.connect();
      const list = await this.client.list(remotePath);
      await this.disconnect();
      
      return list.map(item => ({
        name: item.name,
        type: item.type === 1 ? "file" : item.type === 2 ? "directory" : "other",
        size: item.size,
        modifiedDate: item.modifiedAt ? item.modifiedAt.toISOString() : ""
      }));
    } catch (error) {
      console.error("List directory error:", error);
      throw new Error(`Failed to list directory: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  async downloadFile(remotePath: string): Promise<{filePath: string, content: string}> {
    try {
      await this.connect();
      
      // Create a unique local filename
      const tempFilePath = path.join(this.tempDir, `download-${Date.now()}-${path.basename(remotePath)}`);
      
      // Download the file
      await this.client.downloadTo(tempFilePath, remotePath);
      
      // Read the file content
      const content = fs.readFileSync(tempFilePath, 'utf8');
      
      await this.disconnect();
      
      return {
        filePath: tempFilePath,
        content
      };
    } catch (error) {
      console.error("Download file error:", error);
      throw new Error(`Failed to download file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  async uploadFile(remotePath: string, content: string): Promise<boolean> {
    try {
      await this.connect();
      
      // Create a temporary file with the content
      const tempFilePath = path.join(this.tempDir, `upload-${Date.now()}-${path.basename(remotePath)}`);
      fs.writeFileSync(tempFilePath, content);
      
      // Upload the file
      await this.client.uploadFrom(tempFilePath, remotePath);
      
      // Clean up
      fs.unlinkSync(tempFilePath);
      
      await this.disconnect();
      return true;
    } catch (error) {
      console.error("Upload file error:", error);
      throw new Error(`Failed to upload file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  async createDirectory(remotePath: string): Promise<boolean> {
    try {
      await this.connect();
      await this.client.ensureDir(remotePath);
      await this.disconnect();
      return true;
    } catch (error) {
      console.error("Create directory error:", error);
      throw new Error(`Failed to create directory: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  async deleteFile(remotePath: string): Promise<boolean> {
    try {
      await this.connect();
      await this.client.remove(remotePath);
      await this.disconnect();
      return true;
    } catch (error) {
      console.error("Delete file error:", error);
      throw new Error(`Failed to delete file: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  async deleteDirectory(remotePath: string): Promise<boolean> {
    try {
      await this.connect();
      await this.client.removeDir(remotePath);
      await this.disconnect();
      return true;
    } catch (error) {
      console.error("Delete directory error:", error);
      throw new Error(`Failed to delete directory: ${error instanceof Error ? error.message : String(error)}`);
    }
  }
}
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { FtpClient, FtpConfig } from "./ftp-client.js";

// Get FTP config from environment variables
const ftpConfig: FtpConfig = {
  host: process.env.FTP_HOST || "localhost",
  port: parseInt(process.env.FTP_PORT || "21"),
  user: process.env.FTP_USER || "anonymous",
  password: process.env.FTP_PASSWORD || "",
  secure: process.env.FTP_SECURE?.toLowerCase() === "true"
};

// Initialize FTP client
const ftpClient = new FtpClient(ftpConfig);

// Create server instance
const server = new McpServer({
  name: "mcp-server-ftp",
  version: "1.0.0",
});

// Register list-directory tool
server.tool(
  "list-directory",
  "List contents of an FTP directory",
  {
    remotePath: z.string().describe("Path of the directory on the FTP server"),
  },
  async ({ remotePath }) => {
    try {
      const listing = await ftpClient.listDirectory(remotePath);
      
      // Format the output
      const formatted = listing.map((item) => 
        `${item.type === "directory" ? "[DIR]" : "[FILE]"} ${item.name} ${item.type === "file" ? `(${formatSize(item.size)})` : ""} - ${item.modifiedDate}`
      ).join("\n");
      
      const summary = `Total: ${listing.length} items (${listing.filter(i => i.type === "directory").length} directories, ${listing.filter(i => i.type === "file").length} files)`;
      
      return {
        content: [
          {
            type: "text",
            text: `Directory listing for: ${remotePath}\n\n${formatted}\n\n${summary}`
          }
        ]
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error listing directory: ${error instanceof Error ? error.message : String(error)}`
          }
        ]
      };
    }
  }
);

// Register download-file tool
server.tool(
  "download-file",
  "Download a file from the FTP server",
  {
    remotePath: z.string().describe("Path of the file on the FTP server"),
  },
  async ({ remotePath }) => {
    try {
      const { content } = await ftpClient.downloadFile(remotePath);
      
      return {
        content: [
          {
            type: "text",
            text: `File content of ${remotePath}:\n\n${content}`
          }
        ]
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error downloading file: ${error instanceof Error ? error.message : String(error)}`
          }
        ]
      };
    }
  }
);

// Register upload-file tool
server.tool(
  "upload-file",
  "Upload a file to the FTP server",
  {
    remotePath: z.string().describe("Destination path on the FTP server"),
    content: z.string().describe("Content to upload to the file"),
  },
  async ({ remotePath, content }) => {
    try {
      await ftpClient.uploadFile(remotePath, content);
      
      return {
        content: [
          {
            type: "text",
            text: `File successfully uploaded to ${remotePath}`
          }
        ]
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error uploading file: ${error instanceof Error ? error.message : String(error)}`
          }
        ]
      };
    }
  }
);

// Register create-directory tool
server.tool(
  "create-directory",
  "Create a new directory on the FTP server",
  {
    remotePath: z.string().describe("Path of the directory to create"),
  },
  async ({ remotePath }) => {
    try {
      await ftpClient.createDirectory(remotePath);
      
      return {
        content: [
          {
            type: "text",
            text: `Directory successfully created at ${remotePath}`
          }
        ]
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error creating directory: ${error instanceof Error ? error.message : String(error)}`
          }
        ]
      };
    }
  }
);

// Register delete-file tool
server.tool(
  "delete-file",
  "Delete a file from the FTP server",
  {
    remotePath: z.string().describe("Path of the file to delete"),
  },
  async ({ remotePath }) => {
    try {
      await ftpClient.deleteFile(remotePath);
      
      return {
        content: [
          {
            type: "text",
            text: `File successfully deleted from ${remotePath}`
          }
        ]
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error deleting file: ${error instanceof Error ? error.message : String(error)}`
          }
        ]
      };
    }
  }
);

// Register delete-directory tool
server.tool(
  "delete-directory",
  "Delete a directory from the FTP server",
  {
    remotePath: z.string().describe("Path of the directory to delete"),
  },
  async ({ remotePath }) => {
    try {
      await ftpClient.deleteDirectory(remotePath);
      
      return {
        content: [
          {
            type: "text",
            text: `Directory successfully deleted from ${remotePath}`
          }
        ]
      };
    } catch (error) {
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Error deleting directory: ${error instanceof Error ? error.message : String(error)}`
          }
        ]
      };
    }
  }
);

// Helper function to format file sizes
function formatSize(bytes: number): string {
  if (bytes < 1024) return bytes + " B";
  else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
  else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + " MB";
  else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
}

// Initialize and run the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("FTP MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
```