MCP Filesystem Server
by safurrier
Verified
- mcp-filesystem
- tests
"""End-to-end tests for MCP Filesystem server.
These tests verify the system works correctly from client to server,
simulating how it would be used in a real environment.
"""
import json
from pathlib import Path
import tempfile
import shutil
import pytest
from mcp_filesystem.server import (
mcp,
read_file,
write_file,
list_directory,
edit_file_at_line,
grep_files,
read_file_lines,
directory_tree
)
class MockLifespanContext:
"""Mock lifespan context for the MCP server."""
def __init__(self, test_dir):
from mcp_filesystem.security import PathValidator
from mcp_filesystem.operations import FileOperations
from mcp_filesystem.grep import GrepTools
from mcp_filesystem.advanced import AdvancedFileOperations
self.validator = PathValidator([str(test_dir)])
self.operations = FileOperations(self.validator)
self.grep = GrepTools(self.validator)
self.advanced = AdvancedFileOperations(self.validator, self.operations)
self.allowed_dirs = [str(test_dir)]
class MockRequestContext:
"""Mock request context for the MCP server."""
def __init__(self, lifespan_context):
self.lifespan_context = lifespan_context
class MockContext:
"""Mock context for MCP server."""
def __init__(self, test_dir):
self.lifespan_context = MockLifespanContext(test_dir)
self.request_context = MockRequestContext(self.lifespan_context)
@pytest.fixture
def test_environment():
"""Create a test environment with files and MCP server."""
# Create a temporary directory
temp_dir = tempfile.mkdtemp(prefix="mcp_fs_e2e_")
temp_path = Path(temp_dir)
# Create test files
test_file = temp_path / "test.txt"
test_file.write_text("Line 1\nLine 2\nLine 3\n")
# Create a subdirectory with a file
subdir = temp_path / "subdir"
subdir.mkdir()
subdir_file = subdir / "subfile.txt"
subdir_file.write_text("Subdir file content")
# No need to create a server instance as we'll use the function directly
# Configure the test environment with allowed directories
from mcp_filesystem.server import _components_cache
_components_cache.clear() # Clear cache to ensure clean test state
# Set environment variable for allowed dirs
import os
old_env = os.environ.get("MCP_ALLOWED_DIRS", "")
os.environ["MCP_ALLOWED_DIRS"] = str(temp_path)
# Create mock context
mock_ctx = MockContext(temp_path)
# Yield the environment for tests to use
result = {
"test_dir": temp_path,
"mock_ctx": mock_ctx,
"test_file": test_file,
"subdir_file": subdir_file,
}
yield result
# Clean up
# Restore environment
if old_env:
os.environ["MCP_ALLOWED_DIRS"] = old_env
else:
os.environ.pop("MCP_ALLOWED_DIRS", None)
# Clear component cache
_components_cache.clear()
# Remove temp directory
shutil.rmtree(temp_dir)
@pytest.mark.asyncio
class TestEndToEnd:
"""End-to-end tests for the MCP Filesystem server.
These tests simulate client interactions with the server.
"""
async def test_read_file_e2e(self, test_environment):
"""Test reading a file through the server API."""
# Arrange
test_file = test_environment["test_file"]
mock_ctx = test_environment["mock_ctx"]
# Act
result = await read_file(str(test_file), mock_ctx)
# Assert
assert "Line 1" in result
assert "Line 2" in result
assert "Line 3" in result
async def test_write_file_e2e(self, test_environment):
"""Test writing a file through the server API."""
# Arrange
test_dir = test_environment["test_dir"]
mock_ctx = test_environment["mock_ctx"]
new_file_path = test_dir / "new_e2e_file.txt"
new_content = "Content created in E2E test"
# Act
result = await write_file(str(new_file_path), new_content, mock_ctx)
# Assert
assert "Successfully" in result
assert new_file_path.exists()
assert new_file_path.read_text() == new_content
async def test_list_directory_e2e(self, test_environment):
"""Test listing a directory through the server API."""
# Arrange
test_dir = test_environment["test_dir"]
mock_ctx = test_environment["mock_ctx"]
# Act - Use JSON format for consistent parsing
result = await list_directory(str(test_dir), mock_ctx, format="json")
# Parse the JSON result
entries = json.loads(result)
# Assert
assert len(entries) >= 2 # At least test.txt and subdir
# Find files and directories using the actual API fields (is_directory, is_file)
file_entries = [e for e in entries if e["is_file"] is True]
dir_entries = [e for e in entries if e["is_directory"] is True]
assert any(e["name"] == "test.txt" for e in file_entries)
assert any(e["name"] == "subdir" for e in dir_entries)
async def test_read_file_lines_e2e(self, test_environment):
"""Test reading specific lines from a file through the server API."""
# Arrange
test_file = test_environment["test_file"]
mock_ctx = test_environment["mock_ctx"]
# Act - using offset/limit instead of start_line/end_line
result = await read_file_lines(
str(test_file),
mock_ctx,
offset=1, # 0-based, corresponds to line 2
limit=1,
)
# Assert
assert "Line 2" in result
assert "Line 1" not in result
assert "Line 3" not in result
async def test_read_file_lines_out_of_range_e2e(self, test_environment):
"""Test reading out of range lines from a file through the server API."""
# Arrange
test_file = test_environment["test_file"]
mock_ctx = test_environment["mock_ctx"]
# Act - request beyond file end
result = await read_file_lines(
str(test_file),
mock_ctx,
offset=10, # Beyond file end
)
# Assert - should get a meaningful message, not an error
assert "No content found" in result
async def test_grep_files_e2e(self, test_environment):
"""Test searching files with grep through the server API."""
# Arrange
test_dir = test_environment["test_dir"]
mock_ctx = test_environment["mock_ctx"]
# First add a file with specific content to search for
grep_test_file = test_dir / "grep_test.txt"
grep_test_file.write_text(
"This file contains the search term we're looking for.\nAnother line without the term."
)
# Act
result = await grep_files(
str(test_dir), "search term", mock_ctx
)
# Assert
assert "search term" in result
assert "grep_test.txt" in result
assert "1 matches" in result or "1 match" in result
async def test_edit_file_at_line_e2e(self, test_environment):
"""Test editing a specific line in a file through the server API."""
# Arrange
test_file = test_environment["test_file"]
mock_ctx = test_environment["mock_ctx"]
# Get the current file state
test_file.read_text() # Ensure file exists
# Prepare edit
line_edits = [
{"line_number": 2, "action": "replace", "content": "Modified Line 2"}
]
# Act
result = await edit_file_at_line(
str(test_file), line_edits, mock_ctx
)
# Assert
assert "Line 2" in result or "line 2" in result.lower()
# Check file content
new_content = test_file.read_text()
assert "Modified Line 2" in new_content
assert "Line 1" in new_content # Line 1 should be unchanged
assert "Line 3" in new_content # Line 3 should be unchanged
async def test_edit_file_at_line_with_verification_e2e(self, test_environment):
"""Test editing a file with content verification through the server API."""
# Arrange
test_file = test_environment["test_file"]
mock_ctx = test_environment["mock_ctx"]
# Reset content to known state
original_content = "Line 1\nLine 2\nLine 3\n"
test_file.write_text(original_content)
# Prepare edit with incorrect expected content
line_edits = [
{
"line_number": 2,
"action": "replace",
"content": "Modified Line 2",
"expected_content": "This is not the actual content",
}
]
# Act
result = await edit_file_at_line(
str(test_file), line_edits, mock_ctx, abort_on_verification_failure=True
)
# Assert
assert "Verification failed" in result or "verification" in result.lower()
# File content should remain unchanged
assert test_file.read_text() == original_content
async def test_edit_file_at_line_with_relative_numbers_e2e(self, test_environment):
"""Test editing a file with relative line numbers through the server API."""
# Arrange
test_file = test_environment["test_file"]
mock_ctx = test_environment["mock_ctx"]
# Reset content to known state
original_content = "Line 1\nLine 2\nLine 3\n"
test_file.write_text(original_content)
# Prepare edit using relative line numbers
line_edits = [
{
"line_number": 1, # Relative line number (offset + 1 = line 3)
"action": "replace",
"content": "Modified Line 3 Using Relative Numbering",
}
]
# Act
result = await edit_file_at_line(
str(test_file),
line_edits,
mock_ctx,
offset=1, # Start at line 2
relative_line_numbers=True,
)
# Assert
assert "Applied" in result
# File content should have line 3 changed
new_content = test_file.read_text().splitlines()
assert new_content[2] == "Modified Line 3 Using Relative Numbering"
assert new_content[0] == "Line 1" # Line 1 unchanged
assert new_content[1] == "Line 2" # Line 2 unchanged
async def test_directory_tree_e2e(self, test_environment):
"""Test generating a directory tree through the server API."""
# Arrange
test_dir = test_environment["test_dir"]
mock_ctx = test_environment["mock_ctx"]
# Act - Use JSON format for consistent parsing
result = await directory_tree(str(test_dir), mock_ctx, format="json")
# Parse JSON result
tree = json.loads(result)
# Assert
assert tree["type"] == "directory"
assert tree["name"] == test_dir.name
# Check that children exist and have the expected structure
assert "children" in tree
assert len(tree["children"]) >= 2 # At least test.txt and subdir
# Find the subdir in the tree
subdir = next((c for c in tree["children"] if c["name"] == "subdir"), None)
assert subdir is not None
assert subdir["type"] == "directory"
assert "children" in subdir