diff --git a/.gitignore b/.gitignore index 2000d62..ee2951b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ docker/.env .aider* CLAUDE.md docs/** + +# AI conversation artifacts +context/** +AI_CHANGELOG.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9adc137..501783b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,11 @@ -# Contributing to Python Collab Template +# Contributing to MCP Filesystem Thank you for your interest in contributing to this project! ## Getting Started 1. Fork the repository -2. Clone your fork: `git clone git@github.com:your-username/python-collab-template.git` +2. Clone your fork: `git clone git@github.com:your-username/mcp-filesystem.git` 3. Create a new branch: `git checkout -b feature-name` 4. Make your changes 5. Run quality checks: `make check` diff --git a/README.md b/README.md index 61379b5..2ab8dbf 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,174 @@ # MCP Filesystem Server -[![PyPI - Version](https://img.shields.io/pypi/v/mcp-filesystem.svg)](https://pypi.org/project/mcp-filesystem) [![License](https://img.shields.io/github/license/safurrier/mcp-filesystem.svg)](https://github.com/safurrier/mcp-filesystem/blob/main/LICENSE) -A powerful Model Context Protocol (MCP) server for filesystem operations that provides Claude and other MCP clients with secure access to files and directories. +A powerful Model Context Protocol (MCP) server for filesystem operations optimized for intelligent interaction with large files and filesystems. It provides secure access to files and directories with smart context management to maximize efficiency when working with extensive data. -## Features +## Why MCP-Filesystem? + +- **Smart Context Management**: Work efficiently with large files and filesystems + - Partial reading to focus only on relevant content + - Precise context control for finding exactly what you need + - Token-efficient search results with pagination + - Multi-file operations to reduce request overhead + +- **Intelligent File Operations**: + - Line-targeted reading with configurable context windows + - Advanced editing with content verification to prevent conflicts + - Fine-grained search capabilities that exceed standard grep + - Relative line references for precise file manipulation + +## Key Features - **Secure File Access**: Only allows operations within explicitly allowed directories - **Comprehensive Operations**: Full set of file system capabilities - Standard operations (read, write, list, move, delete) - Enhanced operations (tree visualization, duplicate finding, etc.) - Advanced search with grep integration (uses ripgrep when available) - - Context control (like grep's -A/-B/-C options) + - Context control (like grep's -A/-B/-C options) - Result pagination for large result sets - Line-targeted operations with content verification and relative line numbers - **Performance Optimized**: - Efficiently handles large files and directories - Ripgrep integration for blazing fast searches - Line-targeted operations to avoid loading entire files -- **Claude Integration**: Easily installs in Claude Desktop +- **Comprehensive Testing**: 75+ tests with behavior-driven approach - **Cross-Platform**: Works on Windows, macOS, and Linux -## Installation +## Quickstart Guide + +### 1. Clone and Setup + +First, install uv if you haven't already: -### From PyPI ```bash -# With pip -pip install mcp-filesystem +# Install uv using the official installer +curl -fsSL https://raw.githubusercontent.com/astral-sh/uv/main/install.sh | bash -# With uv (recommended for Claude Desktop) -uv pip install mcp-filesystem +# Or with pipx +pipx install uv ``` -### From Source +Then clone the repository and install dependencies: + ```bash -# With pip +# Clone the repository git clone https://github.com/safurrier/mcp-filesystem.git cd mcp-filesystem -pip install -e . -# With uv (recommended for Claude Desktop) -git clone https://github.com/safurrier/mcp-filesystem.git -cd mcp-filesystem -uv pip install -e . +# Install dependencies with uv +uv pip sync requirements.txt requirements-dev.txt ``` -## Quick Start +### 2. Get Absolute Paths -Run the server with access to the current directory: +You'll need absolute paths both for the repository location and any directories you want to access: ```bash -mcp-filesystem run +# Get the absolute path to the repository +REPO_PATH=$(pwd) +echo "Repository path: $REPO_PATH" + +# Get absolute paths to directories you want to access +realpath ~/Documents +realpath ~/Downloads +# Or on systems without realpath: +echo "$(cd ~/Documents && pwd)" ``` -Allow access to specific directories: +### 3. Configure Claude Desktop -```bash -mcp-filesystem run /path/to/dir1 /path/to/dir2 +Open your Claude Desktop configuration file: +- On macOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` +- On Windows: `%APPDATA%/Claude/claude_desktop_config.json` + +Add the following configuration (substitute your actual paths): + +```json +{ + "mcpServers": { + "mcp-filesystem": { + "command": "uv", + "args": [ + "--directory", + "/absolute/path/to/mcp-filesystem", + "run", + "run_server.py", + "/absolute/path/to/dir1", + "/absolute/path/to/dir2" + ] + } + } +} ``` -Use SSE transport instead of stdio: +> **Important**: All paths must be absolute (full paths from root directory). +> Use `realpath` or `pwd` to ensure you have the correct absolute paths. + +### 4. Restart Claude Desktop + +After saving your configuration, restart Claude Desktop for the changes to take effect. + +## Installation + +## Usage + +### Watch Server Logs + +You can monitor the server logs from Claude Desktop with: ```bash -mcp-filesystem run --transport sse --port 8000 +# On macOS +tail -n 20 -f ~/Library/Logs/Claude/mcp-server-mcp-filesystem.log + +# On Windows (PowerShell) +Get-Content -Path "$env:APPDATA\Claude\Logs\mcp-server-mcp-filesystem.log" -Tail 20 -Wait ``` -## MCP Inspector Usage +This is particularly useful for debugging issues or seeing exactly what Claude is requesting. -When using with MCP Inspector: +### Running the Server -``` -Command: uv -Arguments: --directory /path/to/mcp-filesystem run mcp-filesystem run +Run the server with access to specific directories: + +```bash +# Using uv (recommended) +uv run run_server.py /path/to/dir1 /path/to/dir2 + +# Or using standard Python +python run_server.py /path/to/dir1 /path/to/dir2 + +# Example with actual paths +uv run run_server.py /Users/username/Documents /Users/username/Downloads ``` -Note: The trailing `run` is required as it specifies the subcommand to execute. +#### Options -This server has been refactored to use the new FastMCP SDK for better alignment with current MCP best practices. It now uses a more efficient component caching system and direct decorator pattern rather than a class-based approach. +- `--transport` or `-t`: Transport protocol (stdio or sse, default: stdio) +- `--port` or `-p`: Port for SSE transport (default: 8000) +- `--debug` or `-d`: Enable debug logging +- `--version` or `-v`: Show version information -## Claude Desktop Integration +### Using with MCP Inspector -To install in Claude Desktop: +For interactive testing and debugging with the MCP Inspector: ```bash -# Using mcp CLI -mcp install mcp-filesystem +# Basic usage +npx @modelcontextprotocol/inspector uv run run_server.py /path/to/directory + +# With SSE transport +npx @modelcontextprotocol/inspector uv run run_server.py /path/to/directory --transport sse --port 8080 -# With access to specific directories -mcp install mcp-filesystem --args="/path/to/dir1 /path/to/dir2" +# With debug output +npx @modelcontextprotocol/inspector uv run run_server.py /path/to/directory --debug ``` -Or manually edit your Claude Desktop config file: +This server has been built with the FastMCP SDK for better alignment with current MCP best practices. It uses an efficient component caching system and direct decorator pattern. + +## Claude Desktop Integration + +Edit your Claude Desktop config file to integrate MCP-Filesystem: **Config file location:** - On macOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` @@ -104,10 +181,9 @@ Or manually edit your Claude Desktop config file: "command": "uv", "args": [ "--directory", - "/path/to/mcp-filesystem", + "/path/to/mcp-filesystem/repo", "run", - "mcp-filesystem", - "run" + "run_server.py" ] } } @@ -123,10 +199,9 @@ To allow access to specific directories, add them as additional arguments: "command": "uv", "args": [ "--directory", - "/path/to/mcp-filesystem", - "run", - "mcp-filesystem", + "/path/to/mcp-filesystem/repo", "run", + "run_server.py", "/Users/yourusername/Projects", "/Users/yourusername/Documents" ] @@ -135,7 +210,41 @@ To allow access to specific directories, add them as additional arguments: } ``` -Note: The trailing `run` at the end of the args is required as it specifies the subcommand to execute. +> Note: The `--directory` flag is important as it tells uv where to find the repository containing run_server.py. Replace `/path/to/mcp-filesystem/repo` with the actual path to where you cloned the repository on your system. + +## Development + +### Running Tests + +```bash +# Run all tests +uv run -m pytest tests/ + +# Run specific test file +uv run -m pytest tests/test_operations_unit.py + +# Run with coverage +uv run -m pytest tests/ --cov=mcp_filesystem --cov-report=term-missing +``` + +### Code Style and Quality + +```bash +# Format code +uv run -m ruff format mcp_filesystem + +# Lint code +uv run -m ruff check --fix mcp_filesystem + +# Type check +uv run -m mypy mcp_filesystem + +# Run all checks +uv run -m ruff format mcp_filesystem && \ +uv run -m ruff check --fix mcp_filesystem && \ +uv run -m mypy mcp_filesystem && \ +uv run -m pytest tests --cov=mcp_filesystem +``` ## Available Tools @@ -245,28 +354,74 @@ Arguments: { } ``` -## Efficient Workflow - -The tools are designed to work together efficiently: - -1. Use `grep_files` to find relevant content with precise context control - - Fine-grained control over context lines before/after matches - - Paginate through large result sets efficiently -2. Examine specific sections with `read_file_lines` using offset/limit - - Zero-based indexing with simple offset/limit parameters - - Control exactly how many lines to read -3. Make targeted edits with `edit_file_at_line` with content verification - - Verify content hasn't changed before editing - - Use relative line numbers for regional editing - - Multiple edit actions in a single operation - -This workflow allows Claude to work effectively even with very large codebases by focusing on just the relevant parts while ensuring edits are safe and precise. +## Efficient Workflow for Large Files and Filesystems + +MCP-Filesystem is designed for intelligent interaction with large files and complex filesystems: + +1. **Smart Context Discovery** + - Use `grep_files` to find exactly what you need with precise context control + - Fine-grained control over context lines before/after matches prevents token waste + - Paginate through large result sets efficiently without overwhelming token limits + - Ripgrep integration handles massive filesystems with millions of files and lines + +2. **Targeted Reading** + - Examine only relevant sections with `read_file_lines` using offset/limit + - Zero-based indexing with simple offset/limit parameters for precise content retrieval + - Control exactly how many lines to read to maximize token efficiency + - Read multiple files simultaneously to reduce round-trips + +3. **Precise Editing** + - Make targeted edits with `edit_file_at_line` with content verification + - Verify content hasn't changed before editing to prevent conflicts + - Use relative line numbers for regional editing in complex files + - Multiple edit actions in a single operation for complex changes + - Dry-run capability to preview changes before applying + +4. **Advanced Analysis** + - Use specialized tools like `find_duplicate_files` and `compare_files` + - Generate directory trees with `directory_tree` for quick navigation + - Identify problematic areas with `find_large_files` and `find_empty_directories` + +This workflow is particularly valuable for AI-powered tools that need to work with large files and filesystems. For example, Claude and other advanced AI assistants can leverage these capabilities to efficiently navigate codebases, analyze log files, or work with any large text-based datasets while maintaining token efficiency. + +## Advantages Over Standard Filesystem MCP Servers + +Unlike basic filesystem MCP servers, MCP-Filesystem offers: + +1. **Token Efficiency** + - Smart line-targeted operations avoid loading entire files into context + - Pagination controls for large results prevent context overflow + - Precise grep with context controls (not just whole file searches) + - Multi-file reading reduces round-trip requests + +2. **Intelligent Editing** + - Content verification to prevent edit conflicts + - Line-targeted edits that don't require the entire file + - Relative line number support for easier regional editing + - Dry-run capability to preview changes before applying + +3. **Advanced Search** + - Ripgrep integration for massive filesystem performance + - Context-aware results (not just matches) + - Fine-grained control over what gets returned + - Pattern-based file finding with exclusion support + +4. **Additional Utilities** + - File comparison and deduplication + - Directory size calculation and analysis + - Empty directory identification + - Tree-based directory visualization + +5. **Security Focus** + - Robust path validation and sandboxing + - Protection against path traversal attacks + - Symlink validation and security + - Detailed error reporting without sensitive exposure ## Known Issues and Limitations -- **Regex Escaping**: When using regex patterns with special characters like `\d`, `\w`, or `\s`, you may need to double-escape backslashes (e.g., `\\d`, `\\w`, `\\s`). This is due to how JSON processes escape characters. - **Path Resolution**: Always use absolute paths for the most consistent results. Relative paths might be interpreted relative to the server's working directory rather than the allowed directories. -- **Performance**: For large directories, operations like `find_duplicate_files` might take significant time to complete. +- **Performance**: For large directories, operations like `find_duplicate_files` or recusrive search might take significant time to complete. - **Permission Handling**: The server operates with the same permissions as the user running it. Make sure the server has appropriate permissions for the directories it needs to access. ## Security diff --git a/mcp_filesystem/__init__.py b/mcp_filesystem/__init__.py index 116b197..d0fcbd7 100644 --- a/mcp_filesystem/__init__.py +++ b/mcp_filesystem/__init__.py @@ -7,4 +7,21 @@ from .server import mcp -__all__ = ["mcp"] + +def main() -> None: + """Main entry point for the package.""" + try: + mcp.run() + except KeyboardInterrupt: + import sys + + print("\nShutting down...", file=sys.stderr) + sys.exit(0) + except Exception as e: + import sys + + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +__all__ = ["mcp", "main"] diff --git a/mcp_filesystem/__main__.py b/mcp_filesystem/__main__.py index f50ae8e..419ee32 100644 --- a/mcp_filesystem/__main__.py +++ b/mcp_filesystem/__main__.py @@ -16,9 +16,9 @@ ) -@app.command() -def run( - directory: Annotated[ +@app.callback(invoke_without_command=True) +def main( + directories: Annotated[ Optional[List[str]], typer.Argument( help="Allowed directories (defaults to current directory if none provided)", @@ -49,33 +49,32 @@ def run( help="Enable debug logging", ), ] = False, - name: Annotated[ - Optional[str], + version: Annotated[ + bool, typer.Option( - "--name", - "-n", - help="Server name", + "--version", + "-v", + help="Show version information", ), - ] = None, + ] = False, ) -> None: """Run the MCP Filesystem Server. By default, the server will only allow access to the current directory. You can specify one or more allowed directories as arguments. """ + if version: + show_version() + return + # Set allowed directories in environment for the server to pick up - if directory: - os.environ["MCP_ALLOWED_DIRS"] = os.pathsep.join(directory) + if directories: + os.environ["MCP_ALLOWED_DIRS"] = os.pathsep.join(directories) # Set debug mode if requested if debug: os.environ["FASTMCP_LOG_LEVEL"] = "DEBUG" - # Setting custom name is not supported with FastMCP - # Just log the name for now - if name: - print(f"Using server name: {name}") - try: if transport.lower() == "sse": os.environ["FASTMCP_PORT"] = str(port) @@ -90,8 +89,7 @@ def run( sys.exit(1) -@app.command() -def version() -> None: +def show_version() -> None: """Show version information.""" try: from importlib.metadata import version as get_version diff --git a/pyproject.toml b/pyproject.toml index 2738242..af31a98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ [project.scripts] mcp-filesystem = "mcp_filesystem.__main__:app" +mcp-fs = "mcp_filesystem:main" [project.optional-dependencies] dev = [ diff --git a/run_server.py b/run_server.py index eee8883..5f70f75 100644 --- a/run_server.py +++ b/run_server.py @@ -1,17 +1,113 @@ #!/usr/bin/env python """ -Simple entry point to run the MCP filesystem server. -Usage: uv run run_server.py [dir1] [dir2] ... +Primary entry point to run the MCP filesystem server. +Usage: uv run run_server.py [dir1] [dir2] ... [options] For use with MCP Inspector or Claude Desktop: - Command: uv -- Arguments: --directory /path/to/mcp-filesystem run mcp-filesystem run +- Arguments: --directory /path/to/mcp-filesystem run run_server.py [dir1] [dir2] -Note: The trailing 'run' is required as it specifies the subcommand to execute. +This simplified approach eliminates the need for module invocation with -m flag. """ import sys -from mcp_filesystem.__main__ import app +import os +import typer +from typing import List, Optional +from typing_extensions import Annotated + +from mcp_filesystem.server import mcp + +app = typer.Typer( + name="mcp-filesystem", + help="MCP Filesystem Server", + add_completion=False, +) + +@app.callback(invoke_without_command=True) +def main( + directories: Annotated[ + Optional[List[str]], + typer.Argument( + help="Allowed directories (defaults to current directory if none provided)", + show_default=False, + ), + ] = None, + transport: Annotated[ + str, + typer.Option( + "--transport", + "-t", + help="Transport protocol to use", + ), + ] = "stdio", + port: Annotated[ + int, + typer.Option( + "--port", + "-p", + help="Port for SSE transport", + ), + ] = 8000, + debug: Annotated[ + bool, + typer.Option( + "--debug", + "-d", + help="Enable debug logging", + ), + ] = False, + version: Annotated[ + bool, + typer.Option( + "--version", + "-v", + help="Show version information", + ), + ] = False, +) -> None: + """Run the MCP Filesystem Server. + + By default, the server will only allow access to the current directory. + You can specify one or more allowed directories as arguments. + """ + if version: + show_version() + return + + # Set allowed directories in environment for the server to pick up + if directories: + os.environ["MCP_ALLOWED_DIRS"] = os.pathsep.join(directories) + + # Set debug mode if requested + if debug: + os.environ["FASTMCP_LOG_LEVEL"] = "DEBUG" + + try: + if transport.lower() == "sse": + os.environ["FASTMCP_PORT"] = str(port) + mcp.run(transport="sse") + else: + mcp.run(transport="stdio") + except KeyboardInterrupt: + print("\nShutting down...", file=sys.stderr) + sys.exit(0) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def show_version() -> None: + """Show version information.""" + try: + from importlib.metadata import version as get_version + version = get_version("mcp-filesystem") + except ImportError: + version = "unknown" + + print(f"MCP Filesystem Server v{version}") + print("A Model Context Protocol server for filesystem operations") + if __name__ == "__main__": - sys.exit(app()) \ No newline at end of file + app() \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..3fea23e --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,103 @@ +"""Test the CLI interface for MCP Filesystem. + +This test suite verifies the behavior of the CLI interface, +ensuring it works as expected from a user's perspective. +""" + +import sys +import subprocess +import tempfile +import os +from dataclasses import dataclass +from pathlib import Path +from typing import Optional +import pytest + +@dataclass +class CLITestCase: + """Test case for CLI interface behavior tests.""" + name: str + args: list + expected_in_output: list + expected_not_in_output: Optional[list] = None + expected_returncode: int = 0 + check_stderr: bool = False + +@pytest.fixture +def temp_directory(): + """Create a temporary directory for testing.""" + with tempfile.TemporaryDirectory() as temp_dir: + yield Path(temp_dir) + +def run_cli_command(args, cwd=None): + """Run the CLI command with the given arguments.""" + # Get repo root if no cwd provided + if cwd is None: + cwd = Path(__file__).parent.parent.absolute() + + # Use the run_server.py script directly + cmd = [sys.executable, str(Path(cwd) / "run_server.py")] + args + result = subprocess.run( + cmd, + cwd=str(cwd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False # We'll check the return code ourselves + ) + return result + +@pytest.mark.parametrize("case", [ + CLITestCase( + name="help flag shows options", + args=["--help"], + expected_in_output=[ + "transport", + "port", + "debug", + "directories" # Should accept directories as arguments + ], + expected_not_in_output=["run [OPTIONS]"] # No 'run' subcommand required + ), + CLITestCase( + name="version flag shows version", + args=["--version"], + expected_in_output=["MCP Filesystem Server v"] + ) +]) +def test_cli_behavior(case): + """Test CLI behaviors using table-driven testing approach.""" + # Act + result = run_cli_command(case.args) + + # Assert + output = result.stderr if case.check_stderr else result.stdout + output = output.lower() # Case-insensitive matching to focus on content not format + + # Check expected content is present - focus on behaviors not exact strings + for expected in case.expected_in_output: + assert expected.lower() in output, f"Expected '{expected}' in output, but it was not found. Output: {output}" + + # Check unexpected content is absent + if case.expected_not_in_output: + for unexpected in case.expected_not_in_output: + assert unexpected.lower() not in output, f"Found unexpected '{unexpected}' in output. Output: {output}" + + # Check return code + assert result.returncode == case.expected_returncode, \ + f"Expected return code {case.expected_returncode}, got {result.returncode}. Error: {result.stderr}" + +def test_direct_script_execution(): + """Test direct script execution without module invocation. + + This is the key behavioral change - users can run run_server.py directly + without needing to use the -m module flag. + """ + # Arrange - using the default repo root in run_cli_command + + # Act - Using --version as a simple, reliable command to test the interface + result = run_cli_command(["--version"]) + + # Assert - check that the command is recognized and executed successfully + assert result.returncode == 0, f"Command failed: {result.stderr}" + assert "MCP Filesystem Server" in result.stdout, "Version information not found in output" \ No newline at end of file