"""
File Tool - File system operations with path restrictions.
Demonstrates:
- Configuration-aware tools (allowed paths)
- Error handling patterns
- Security considerations
"""
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP
from ..config import ServerConfig
def register(mcp: "FastMCP", config: "ServerConfig") -> None:
"""Register file tools with the server."""
@mcp.tool()
def list_directory(path: str) -> str:
"""
List contents of a directory.
Requires MCP_ALLOWED_PATHS to be configured with permitted directories.
Args:
path: Directory path to list
Returns:
Formatted directory listing or error message
"""
if not config.is_path_allowed(path):
return f"Error: Path '{path}' is not in allowed directories. Configure MCP_ALLOWED_PATHS."
try:
target = Path(path)
if not target.exists():
return f"Error: Path '{path}' does not exist"
if not target.is_dir():
return f"Error: Path '{path}' is not a directory"
items = []
for item in sorted(target.iterdir()):
item_type = "DIR" if item.is_dir() else "FILE"
items.append(f"[{item_type}] {item.name}")
if items:
return f"Contents of {path}:\n" + "\n".join(items)
return f"Directory {path} is empty"
except PermissionError:
return f"Error: Permission denied accessing '{path}'"
except Exception as e:
return f"Error: {str(e)}"
@mcp.tool()
def read_file(path: str, max_lines: int = 100) -> str:
"""
Read contents of a text file.
Requires MCP_ALLOWED_PATHS to be configured with permitted directories.
Args:
path: File path to read
max_lines: Maximum number of lines to return (default: 100)
Returns:
File contents or error message
"""
if not config.is_path_allowed(path):
return f"Error: Path '{path}' is not in allowed directories. Configure MCP_ALLOWED_PATHS."
try:
target = Path(path)
if not target.exists():
return f"Error: File '{path}' does not exist"
if not target.is_file():
return f"Error: Path '{path}' is not a file"
with open(target, "r", encoding="utf-8", errors="replace") as f:
lines = []
for i, line in enumerate(f):
if i >= max_lines:
lines.append(f"\n... truncated after {max_lines} lines ...")
break
lines.append(line.rstrip())
return "\n".join(lines)
except PermissionError:
return f"Error: Permission denied reading '{path}'"
except Exception as e:
return f"Error: {str(e)}"
@mcp.tool()
def get_file_info(path: str) -> str:
"""
Get information about a file or directory.
Requires MCP_ALLOWED_PATHS to be configured with permitted directories.
Args:
path: Path to get info about
Returns:
File information or error message
"""
if not config.is_path_allowed(path):
return f"Error: Path '{path}' is not in allowed directories. Configure MCP_ALLOWED_PATHS."
try:
target = Path(path)
if not target.exists():
return f"Error: Path '{path}' does not exist"
stat = target.stat()
info = [
f"Path: {target.resolve()}",
f"Type: {'Directory' if target.is_dir() else 'File'}",
f"Size: {stat.st_size:,} bytes",
]
return "\n".join(info)
except PermissionError:
return f"Error: Permission denied accessing '{path}'"
except Exception as e:
return f"Error: {str(e)}"
@mcp.tool()
def get_allowed_paths() -> str:
"""
Get the list of allowed file system paths.
Returns:
List of configured allowed paths or instructions to configure
"""
if not config.allowed_paths:
return "No paths configured. Set MCP_ALLOWED_PATHS environment variable."
return "Allowed paths:\n" + "\n".join(f" - {p}" for p in config.allowed_paths)