Skip to content

Fix hash calculation bug in read_multiple_ranges and add main execution block #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
.DEFAULT_GOAL := all

test:
pytest
uv run pytest

install:
uv sync --all-extras

coverage:
pytest --cov=mcp_text_editor --cov-report=term-missing
uv run pytest --cov=mcp_text_editor --cov-report=term-missing

format:
black src tests
isort src tests
ruff check --fix src tests
uv run black src tests
uv run isort src tests
uv run ruff check --fix src tests


lint:
black --check src tests
isort --check src tests
ruff check src tests
uv run black --check src tests
uv run isort --check src tests
uv run ruff check src tests

typecheck:
mypy src tests
uv run mypy src tests

# Run all checks required before pushing
check: lint typecheck
Expand Down
131 changes: 78 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

[![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)
[![Glama MCP Server](https://glama.ai/mcp/servers/k44dnvso10/badge)](https://glama.ai/mcp/servers/k44dnvso10)

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.

<a href="https://glama.ai/mcp/servers/k44dnvso10"><img width="380" height="200" src="https://glama.ai/mcp/servers/k44dnvso10/badge" alt="mcp-text-editor MCP server" /></a>

## Quick Start for Claude.app Users

To use this editor with Claude.app, add the following configuration to your prompt:
Expand All @@ -18,13 +17,12 @@ code ~/Library/Application\ Support/Claude/claude_desktop_config.json
```json
{
"mcpServers": {

"text-editor": {
"command": "uvx",
"args": [
"mcp-text-editor"
]
},
}
}
}
```
Expand All @@ -40,7 +38,8 @@ MCP Text Editor Server is designed to facilitate safe and efficient line-based t
- Optimized for LLM tool integration
- Safe concurrent editing with hash-based validation
- Atomic multi-file operations
- Robust error handling and recovery mechanisms
- Robust error handling with custom error types
- Comprehensive encoding support (utf-8, shift_jis, latin1, etc.)

## Features

Expand Down Expand Up @@ -83,8 +82,20 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
uv pip install -e ".[dev]"
```

## Requirements

- Python 3.13+
- POSIX-compliant operating system (Linux, macOS, etc.) or Windows
- File system permissions for read/write operations

## Installation

### Run via uvx

```bash
uvx mcp-text-editor
```

### Installing via Smithery

To install Text Editor Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-text-editor):
Expand All @@ -94,14 +105,26 @@ npx -y @smithery/cli install mcp-text-editor --client claude
```

### Manual Installation

1. Install Python 3.13+

```bash
pip install -e .
pyenv install 3.13.0
pyenv local 3.13.0
```

For development:
2. Install uv (recommended) or pip

```bash
pip install -e ".[dev]"
curl -LsSf https://astral.sh/uv/install.sh | sh
```

3. Create virtual environment and install dependencies

```bash
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
uv pip install -e ".[dev]"
```

## Usage
Expand All @@ -114,7 +137,7 @@ python -m mcp_text_editor

### MCP Tools

The server provides two main tools:
The server provides several tools for text file manipulation:

#### get_text_file_contents

Expand All @@ -126,7 +149,8 @@ Get the contents of one or more text files with line range specification.
{
"file_path": "path/to/file.txt",
"line_start": 1,
"line_end": 10
"line_end": 10,
"encoding": "utf-8" // Optional, defaults to utf-8
}
```

Expand All @@ -140,7 +164,8 @@ Get the contents of one or more text files with line range specification.
"ranges": [
{"start": 1, "end": 10},
{"start": 20, "end": 30}
]
],
"encoding": "shift_jis" // Optional, defaults to utf-8
},
{
"file_path": "file2.txt",
Expand Down Expand Up @@ -178,16 +203,16 @@ Parameters:
"file1.txt": [
{
"content": "Lines 1-10 content",
"start_line": 1,
"end_line": 10,
"start": 1,
"end": 10,
"hash": "sha256-hash-1",
"total_lines": 50,
"content_size": 512
},
{
"content": "Lines 20-30 content",
"start_line": 20,
"end_line": 30,
"start": 20,
"end": 30,
"hash": "sha256-hash-2",
"total_lines": 50,
"content_size": 512
Expand All @@ -196,8 +221,8 @@ Parameters:
"file2.txt": [
{
"content": "Lines 5-15 content",
"start_line": 5,
"end_line": 15,
"start": 5,
"end": 15,
"hash": "sha256-hash-3",
"total_lines": 30,
"content_size": 256
Expand All @@ -206,39 +231,31 @@ Parameters:
}
```

#### edit_text_file_contents
#### patch_text_file_contents

Edit text file contents with conflict detection. Supports editing multiple files in a single operation.
Apply patches to text files with robust error handling and conflict detection. Supports editing multiple files in a single operation.

**Request Format:**

```json
{
"files": [
{
"path": "file1.txt",
"file_path": "file1.txt",
"hash": "sha256-hash-from-get-contents",
"encoding": "utf-8", // Optional, defaults to utf-8
"patches": [
{
"line_start": 5,
"line_end": 8,
"start": 5,
"end": 8,
"range_hash": "sha256-hash-of-content-being-replaced",
"contents": "New content for lines 5-8\n"
},
{
"line_start": 15,
"line_end": 15,
"contents": "Single line replacement\n"
}
]
},
{
"path": "file2.txt",
"hash": "sha256-hash-from-get-contents",
"patches": [
{
"line_start": 1,
"line_end": 3,
"contents": "Replace first three lines\n"
"start": 15,
"end": null, // null means end of file
"range_hash": "sha256-hash-of-content-being-replaced",
"contents": "Content to append\n"
}
]
}
Expand All @@ -247,11 +264,11 @@ Edit text file contents with conflict detection. Supports editing multiple files
```

Important Notes:
1. Always get the current hash using get_text_file_contents before editing
1. Always get the current hash and range_hash using get_text_file_contents before editing
2. Patches are applied from bottom to top to handle line number shifts correctly
3. Patches must not overlap within the same file
4. Line numbers are 1-based
5. If original content ends with newline, ensure patch content also ends with newline
5. `end: null` can be used to append content to the end of file
6. File encoding must match the encoding used in get_text_file_contents

**Success Response:**
Expand All @@ -261,30 +278,31 @@ Important Notes:
"file1.txt": {
"result": "ok",
"hash": "sha256-hash-of-new-contents"
},
"file2.txt": {
"result": "ok",
"hash": "sha256-hash-of-new-contents"
}
}
```

**Error Response:**
**Error Response with Hints:**

```json
{
"file1.txt": {
"result": "error",
"reason": "File not found",
"hash": null
},
"file2.txt": {
"reason": "Content hash mismatch",
"suggestion": "get", // Suggests using get_text_file_contents
"hint": "Please run get_text_file_contents first to get current content and hashes"
}
}
```

"result": "error",
"reason": "Content hash mismatch - file was modified",
"hash": "current-hash",
"content": "Current file content"

}
}

```

### Common Usage Pattern
Expand Down Expand Up @@ -360,26 +378,33 @@ The server handles various error cases:
- Check file and directory permissions
- Ensure the server process has necessary read/write access

2. Hash Mismatch Errors
2. Hash Mismatch and Range Hash Errors
- The file was modified by another process
- Fetch latest content and retry the operation
- Content being replaced has changed
- Run get_text_file_contents to get fresh hashes

3. Encoding Issues
- Verify file encoding matches the specified encoding
- Use utf-8 for new files
- Check for BOM markers in files

3. Connection Issues
4. Connection Issues
- Verify the server is running and accessible
- Check network configuration and firewall settings

4. Performance Issues
5. Performance Issues
- Consider using smaller line ranges for large files
- Monitor system resources (memory, disk space)
- Use appropriate encoding for file type

## Development

### Setup

1. Clone the repository
2. Create and activate a Python virtual environment
3. Install development dependencies: `pip install -e ".[dev]"`
4. Run tests: `pytest`
3. Install development dependencies: `uv pip install -e ".[dev]"`
4. Run tests: `make all`

### Code Quality Tools

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = [
]
dependencies = [
"asyncio>=3.4.3",
"mcp>=1.1.2",
"mcp>=1.2.0",
"chardet>=5.2.0",
]
requires-python = ">=3.13"
Expand Down Expand Up @@ -38,7 +38,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
asyncio_mode = "strict"
asyncio_mode = "auto"
testpaths = "tests"
asyncio_default_fixture_loop_scope = "function"
pythonpath = ["src"]
Expand Down
9 changes: 1 addition & 8 deletions src/mcp_text_editor/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,18 @@

from typing import Any, Dict, Sequence

from mcp.types import TextContent, Tool
from mcp.types import TextContent

from ..text_editor import TextEditor


class BaseHandler:
"""Base class for handlers."""

name: str = ""
description: str = ""

def __init__(self, editor: TextEditor | None = None):
"""Initialize the handler."""
self.editor = editor if editor is not None else TextEditor()

def get_tool_description(self) -> Tool:
"""Get the tool description."""
raise NotImplementedError

async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
"""Execute the tool with given arguments."""
raise NotImplementedError
Loading