Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
tumf committed Jan 3, 2025
2 parents a20cf7c + 32cba5b commit 6db7be4
Show file tree
Hide file tree
Showing 15 changed files with 631 additions and 136 deletions.
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## [1.1.0] - 2024-12-23

### Added

- New text file manipulation operations:
- `insert_text_file_contents`: Insert content at specific positions
- `create_text_file`: Create new text files
- `append_text_file_contents`: Append content to existing files
- `delete_text_file_contents`: Delete specified ranges of text
- `patch_text_file_contents`: Apply multiple patches to text files
- Enhanced error messages with useful suggestions for alternative editing methods

### Changed

- Unified parameter naming: renamed 'path' to 'file_path' across all APIs
- Improved handler organization by moving them to separate directory
- Made 'end' parameter required when not in append mode
- Enhanced validation for required parameters and file path checks
- Removed 'edit_text_file_contents' tool in favor of more specific operations
- Improved JSON serialization for handler responses

### Fixed

- Delete operation now uses dedicated deletion method instead of empty content replacement
- Improved range validation in delete operations
- Enhanced error handling across all operations
- Removed file hash from error responses for better clarity
- Fixed concurrency control with proper hash validation

## [1.0.2] - 2024-12-22

### Fixed
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# MCP Text Editor Server

[![codecov](https://codecov.io/gh/tumf/mcp-text-editor/branch/main/graph/badge.svg?token=52D51U0ZUR)](https://codecov.io/gh/tumf/mcp-text-editor)
[![smithery badge](https://smithery.ai/badge/mcp-text-editor)](https://smithery.ai/server/mcp-text-editor)

A Model Context Protocol (MCP) server that provides line-oriented text file editing capabilities through a standardized API. Optimized for LLM tools with efficient partial file access to minimize token usage.

Expand Down Expand Up @@ -84,6 +85,15 @@ uv pip install -e ".[dev]"

## Installation

### Installing via Smithery

To install Text Editor Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-text-editor):

```bash
npx -y @smithery/cli install mcp-text-editor --client claude
```

### Manual Installation
```bash
pip install -e .
```
Expand Down
6 changes: 3 additions & 3 deletions src/mcp_text_editor/handlers/append_text_file_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def get_tool_description(self) -> Tool:
inputSchema={
"type": "object",
"properties": {
"path": {
"file_path": {
"type": "string",
"description": "Path to the text file. File path must be absolute.",
},
Expand All @@ -45,7 +45,7 @@ def get_tool_description(self) -> Tool:
"default": "utf-8",
},
},
"required": ["path", "contents", "file_hash"],
"required": ["file_path", "contents", "file_hash"],
},
)

Expand Down Expand Up @@ -87,7 +87,7 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
# Create patch for append operation
result = await self.editor.edit_file_contents(
file_path,
expected_hash=arguments["file_hash"],
expected_file_hash=arguments["file_hash"],
patches=[
{
"start": total_lines + 1,
Expand Down
4 changes: 2 additions & 2 deletions src/mcp_text_editor/handlers/create_text_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_tool_description(self) -> Tool:
"default": "utf-8",
},
},
"required": ["path", "contents"],
"required": ["file_path", "contents"],
},
)

Expand All @@ -68,7 +68,7 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
# Create new file using edit_file_contents with empty expected_hash
result = await self.editor.edit_file_contents(
file_path,
expected_hash="", # Empty hash for new file
expected_file_hash="", # Empty hash for new file
patches=[
{
"start": 1,
Expand Down
36 changes: 22 additions & 14 deletions src/mcp_text_editor/handlers/delete_text_file_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from mcp.types import TextContent, Tool

from ..models import DeleteTextFileContentsRequest, FileRange
from .base import BaseHandler

logger = logging.getLogger("mcp-text-editor")
Expand Down Expand Up @@ -88,26 +89,33 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:

encoding = arguments.get("encoding", "utf-8")

# Create patches for deletion (replacing content with empty string)
patches = [
{
"start": r["start"],
"end": r["end"],
"contents": "",
"range_hash": r["range_hash"],
}
# Create file ranges for deletion
ranges = [
FileRange(
start=r["start"], end=r.get("end"), range_hash=r["range_hash"]
)
for r in arguments["ranges"]
]

# Use the existing edit_file_contents method
result = await self.editor.edit_file_contents(
file_path,
expected_hash=arguments["file_hash"],
patches=patches,
# Create delete request
request = DeleteTextFileContentsRequest(
file_path=file_path,
file_hash=arguments["file_hash"],
ranges=ranges,
encoding=encoding,
)

return [TextContent(type="text", text=json.dumps(result, indent=2))]
# Execute deletion using the service
result_dict = self.editor.service.delete_text_file_contents(request)

# Convert EditResults to dictionaries
serializable_result = {}
for file_path, edit_result in result_dict.items():
serializable_result[file_path] = edit_result.to_dict()

return [
TextContent(type="text", text=json.dumps(serializable_result, indent=2))
]

except Exception as e:
logger.error(f"Error processing request: {str(e)}")
Expand Down
6 changes: 3 additions & 3 deletions src/mcp_text_editor/handlers/patch_text_file_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def get_tool_description(self) -> Tool:
"description": "Starting line number (1-based).it should match the range hash.",
},
"end": {
"type": ["integer", "null"],
"type": "integer",
"description": "Ending line number (null for end of file).it should match the range hash.",
},
"contents": {
Expand All @@ -58,7 +58,7 @@ def get_tool_description(self) -> Tool:
"description": "Hash of the content being replaced. it should get from get_text_file_contents tool with the same start and end.",
},
},
"required": ["start", "contents", "range_hash"],
"required": ["start", "end", "contents", "range_hash"],
},
},
"encoding": {
Expand Down Expand Up @@ -94,7 +94,7 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
# Apply patches using editor.edit_file_contents
result = await self.editor.edit_file_contents(
file_path=file_path,
expected_hash=arguments["file_hash"],
expected_file_hash=arguments["file_hash"],
patches=arguments["patches"],
encoding=encoding,
)
Expand Down
12 changes: 7 additions & 5 deletions src/mcp_text_editor/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@ class EditPatch(BaseModel):
)

@model_validator(mode="after")
def validate_end_line(self) -> "EditPatch":
"""Validate that end line is present when not in append mode."""
def validate_range_hash(self) -> "EditPatch":
"""Validate that range_hash is set and handle end field validation."""
# range_hash must be explicitly set
if self.range_hash is None:
raise ValueError("range_hash is required")

# For modifications (non-empty range_hash), end is required
if self.range_hash != "" and self.end is None:
raise ValueError("end line is required when not in append mode")
# For safety, convert None to the special range hash value
if self.end is None and self.range_hash != "":
# Special case: patch with end=None is allowed
pass

return self


Expand Down
Loading

0 comments on commit 6db7be4

Please sign in to comment.