OpenAI MCP Server
by arthurcolle
- claude_code
- lib
- tools
#!/usr/bin/env python3
# claude_code/lib/tools/file_tools.py
"""File operation tools."""
import os
import logging
from typing import Dict, List, Optional, Any
from .base import tool, ToolRegistry
logger = logging.getLogger(__name__)
@tool(
name="View",
description="Reads a file from the local filesystem. The file_path parameter must be an absolute path, not a relative path.",
parameters={
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to read"
},
"limit": {
"type": "number",
"description": "The number of lines to read. Only provide if the file is too large to read at once."
},
"offset": {
"type": "number",
"description": "The line number to start reading from. Only provide if the file is too large to read at once"
}
},
"required": ["file_path"]
},
category="file"
)
def view_file(file_path: str, limit: Optional[int] = None, offset: Optional[int] = 0) -> str:
"""Read contents of a file.
Args:
file_path: Absolute path to the file
limit: Maximum number of lines to read
offset: Line number to start reading from
Returns:
File contents as a string
Raises:
FileNotFoundError: If the file doesn't exist
PermissionError: If the file can't be read
"""
logger.info(f"Reading file: {file_path} (offset={offset}, limit={limit})")
if not os.path.isabs(file_path):
return f"Error: File path must be absolute: {file_path}"
if not os.path.exists(file_path):
return f"Error: File not found: {file_path}"
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
if limit is not None and offset is not None:
# Skip to offset
for _ in range(offset):
next(f, None)
# Read limited lines
lines = []
for _ in range(limit):
line = next(f, None)
if line is None:
break
lines.append(line)
content = ''.join(lines)
else:
content = f.read()
return content
except Exception as e:
logger.exception(f"Error reading file: {file_path}")
return f"Error reading file: {str(e)}"
@tool(
name="Edit",
description="This is a tool for editing files. For moving or renaming files, you should generally use the Bash tool with the 'mv' command instead.",
parameters={
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to modify"
},
"old_string": {
"type": "string",
"description": "The text to replace"
},
"new_string": {
"type": "string",
"description": "The text to replace it with"
}
},
"required": ["file_path", "old_string", "new_string"]
},
needs_permission=True,
category="file"
)
def edit_file(file_path: str, old_string: str, new_string: str) -> str:
"""Edit a file by replacing text.
Args:
file_path: Absolute path to the file
old_string: Text to replace
new_string: Replacement text
Returns:
Success or error message
Raises:
FileNotFoundError: If the file doesn't exist
PermissionError: If the file can't be modified
"""
logger.info(f"Editing file: {file_path}")
if not os.path.isabs(file_path):
return f"Error: File path must be absolute: {file_path}"
try:
# Create directory if creating new file
if not os.path.exists(os.path.dirname(file_path)) and old_string == "":
os.makedirs(os.path.dirname(file_path), exist_ok=True)
if old_string == "" and not os.path.exists(file_path):
# Creating new file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_string)
return f"Created new file: {file_path}"
# Reading existing file
if not os.path.exists(file_path):
return f"Error: File not found: {file_path}"
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
# Replace string
if old_string not in content:
return f"Error: Could not find the specified text in {file_path}"
# Count occurrences to ensure uniqueness
occurrences = content.count(old_string)
if occurrences > 1:
return f"Error: Found {occurrences} occurrences of the specified text in {file_path}. Please provide more context to uniquely identify the text to replace."
new_content = content.replace(old_string, new_string)
# Write back to file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
return f"Successfully edited {file_path}"
except Exception as e:
logger.exception(f"Error editing file: {file_path}")
return f"Error editing file: {str(e)}"
@tool(
name="Replace",
description="Write a file to the local filesystem. Overwrites the existing file if there is one.",
parameters={
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to write"
},
"content": {
"type": "string",
"description": "The content to write to the file"
}
},
"required": ["file_path", "content"]
},
needs_permission=True,
category="file"
)
def replace_file(file_path: str, content: str) -> str:
"""Replace file contents or create a new file.
Args:
file_path: Absolute path to the file
content: New content for the file
Returns:
Success or error message
Raises:
PermissionError: If the file can't be written
"""
logger.info(f"Replacing file: {file_path}")
if not os.path.isabs(file_path):
return f"Error: File path must be absolute: {file_path}"
try:
# Create directory if it doesn't exist
directory = os.path.dirname(file_path)
if directory and not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)
# Write content to file
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return f"Successfully wrote to {file_path}"
except Exception as e:
logger.exception(f"Error writing file: {file_path}")
return f"Error writing file: {str(e)}"
@tool(
name="MakeDirectory",
description="Create a new directory on the local filesystem.",
parameters={
"type": "object",
"properties": {
"directory_path": {
"type": "string",
"description": "The absolute path to the directory to create"
},
"parents": {
"type": "boolean",
"description": "Whether to create parent directories if they don't exist",
"default": True
},
"mode": {
"type": "integer",
"description": "The file mode (permissions) to set for the directory (octal)",
"default": 0o755
}
},
"required": ["directory_path"]
},
needs_permission=True,
category="file"
)
def make_directory(directory_path: str, parents: bool = True, mode: int = 0o755) -> str:
"""Create a new directory.
Args:
directory_path: Absolute path to the directory to create
parents: Whether to create parent directories
mode: File mode (permissions) to set
Returns:
Success or error message
Raises:
PermissionError: If the directory can't be created
"""
logger.info(f"Creating directory: {directory_path}")
if not os.path.isabs(directory_path):
return f"Error: Directory path must be absolute: {directory_path}"
try:
if os.path.exists(directory_path):
if os.path.isdir(directory_path):
return f"Directory already exists: {directory_path}"
else:
return f"Error: Path exists but is not a directory: {directory_path}"
# Create directory
os.makedirs(directory_path, exist_ok=parents, mode=mode)
return f"Successfully created directory: {directory_path}"
except Exception as e:
logger.exception(f"Error creating directory: {directory_path}")
return f"Error creating directory: {str(e)}"
@tool(
name="ListDirectory",
description="List files and directories in a given path with detailed information.",
parameters={
"type": "object",
"properties": {
"directory_path": {
"type": "string",
"description": "The absolute path to the directory to list"
},
"pattern": {
"type": "string",
"description": "Optional glob pattern to filter files (e.g., '*.py')"
},
"recursive": {
"type": "boolean",
"description": "Whether to list files recursively",
"default": False
},
"show_hidden": {
"type": "boolean",
"description": "Whether to show hidden files (starting with .)",
"default": False
},
"details": {
"type": "boolean",
"description": "Whether to show detailed information (size, permissions, etc.)",
"default": False
}
},
"required": ["directory_path"]
},
category="file"
)
def list_directory(
directory_path: str,
pattern: Optional[str] = None,
recursive: bool = False,
show_hidden: bool = False,
details: bool = False
) -> str:
"""List files and directories with detailed information.
Args:
directory_path: Absolute path to the directory
pattern: Glob pattern to filter files
recursive: Whether to list files recursively
show_hidden: Whether to show hidden files
details: Whether to show detailed information
Returns:
Directory listing as formatted text
"""
logger.info(f"Listing directory: {directory_path}")
if not os.path.isabs(directory_path):
return f"Error: Directory path must be absolute: {directory_path}"
if not os.path.exists(directory_path):
return f"Error: Directory not found: {directory_path}"
if not os.path.isdir(directory_path):
return f"Error: Path is not a directory: {directory_path}"
try:
import glob
import stat
from datetime import datetime
# Build the pattern
if pattern:
if recursive:
search_pattern = os.path.join(directory_path, "**", pattern)
else:
search_pattern = os.path.join(directory_path, pattern)
else:
if recursive:
search_pattern = os.path.join(directory_path, "**")
else:
search_pattern = os.path.join(directory_path, "*")
# Get all matching files
if recursive:
matches = glob.glob(search_pattern, recursive=True)
else:
matches = glob.glob(search_pattern)
# Filter hidden files if needed
if not show_hidden:
matches = [m for m in matches if not os.path.basename(m).startswith('.')]
# Sort by name
matches.sort()
# Format the output
result = []
if details:
# Header
result.append(f"{'Type':<6} {'Permissions':<11} {'Size':<10} {'Modified':<20} {'Name'}")
result.append("-" * 80)
for item_path in matches:
try:
# Get file stats
item_stat = os.stat(item_path)
# Determine type
if os.path.isdir(item_path):
item_type = "dir"
elif os.path.islink(item_path):
item_type = "link"
else:
item_type = "file"
# Format permissions
mode = item_stat.st_mode
perms = ""
for who in "USR", "GRP", "OTH":
for what in "R", "W", "X":
perm = getattr(stat, f"S_I{what}{who}")
perms += what.lower() if mode & perm else "-"
# Format size
size = item_stat.st_size
if size < 1024:
size_str = f"{size}B"
elif size < 1024 * 1024:
size_str = f"{size/1024:.1f}KB"
elif size < 1024 * 1024 * 1024:
size_str = f"{size/(1024*1024):.1f}MB"
else:
size_str = f"{size/(1024*1024*1024):.1f}GB"
# Format modification time
mtime = datetime.fromtimestamp(item_stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
# Format name (relative to the directory)
name = os.path.relpath(item_path, directory_path)
# Add to result
result.append(f"{item_type:<6} {perms:<11} {size_str:<10} {mtime:<20} {name}")
except Exception as e:
result.append(f"Error getting info for {item_path}: {str(e)}")
else:
# Simple listing
dirs = []
files = []
for item_path in matches:
name = os.path.relpath(item_path, directory_path)
if os.path.isdir(item_path):
dirs.append(f"{name}/")
else:
files.append(name)
if dirs:
result.append("Directories:")
for d in dirs:
result.append(f" {d}")
if files:
if dirs:
result.append("")
result.append("Files:")
for f in files:
result.append(f" {f}")
if not result:
return f"No matching items found in {directory_path}"
return "\n".join(result)
except Exception as e:
logger.exception(f"Error listing directory: {directory_path}")
return f"Error listing directory: {str(e)}"
def register_file_tools(registry: ToolRegistry) -> None:
"""Register all file tools with the registry.
Args:
registry: Tool registry to register with
"""
from .base import create_tools_from_functions
file_tools = [
view_file,
edit_file,
replace_file,
make_directory,
list_directory
]
create_tools_from_functions(registry, file_tools)