-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
27 changed files
with
3,133 additions
and
861 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,8 @@ ENV/ | |
*.swo | ||
|
||
# Testing | ||
*.cover | ||
*,cover | ||
.coverage | ||
.coverage.* | ||
.pytest_cache/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,37 @@ | ||
"""MCP Text Editor Server package.""" | ||
|
||
import asyncio | ||
from typing import Any, Dict, List | ||
|
||
from .server import main | ||
from .text_editor import TextEditor | ||
|
||
# Create a global text editor instance | ||
_text_editor = TextEditor() | ||
|
||
|
||
def run() -> None: | ||
"""Run the MCP Text Editor Server.""" | ||
asyncio.run(main()) | ||
|
||
|
||
# Export functions | ||
async def get_text_file_contents( | ||
request: Dict[str, List[Dict[str, Any]]] | ||
) -> Dict[str, Any]: | ||
"""Get text file contents with line range specification.""" | ||
return await _text_editor.read_multiple_ranges( | ||
ranges=request["files"], | ||
encoding="utf-8", | ||
) | ||
|
||
|
||
async def insert_text_file_contents(request: Dict[str, Any]) -> Dict[str, Any]: | ||
"""Insert text content before or after a specific line in a file.""" | ||
return await _text_editor.insert_text_file_contents( | ||
file_path=request["file_path"], | ||
file_hash=request["file_hash"], | ||
after=request.get("after"), | ||
before=request.get("before"), | ||
contents=request["contents"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
"""Handlers for MCP Text Editor.""" | ||
|
||
from .append_text_file_contents import AppendTextFileContentsHandler | ||
from .create_text_file import CreateTextFileHandler | ||
from .delete_text_file_contents import DeleteTextFileContentsHandler | ||
from .get_text_file_contents import GetTextFileContentsHandler | ||
from .insert_text_file_contents import InsertTextFileContentsHandler | ||
from .patch_text_file_contents import PatchTextFileContentsHandler | ||
|
||
__all__ = [ | ||
"AppendTextFileContentsHandler", | ||
"CreateTextFileHandler", | ||
"DeleteTextFileContentsHandler", | ||
"GetTextFileContentsHandler", | ||
"InsertTextFileContentsHandler", | ||
"PatchTextFileContentsHandler", | ||
] |
107 changes: 107 additions & 0 deletions
107
src/mcp_text_editor/handlers/append_text_file_contents.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
"""Handler for appending content to text files.""" | ||
|
||
import json | ||
import logging | ||
import os | ||
import traceback | ||
from typing import Any, Dict, Sequence | ||
|
||
from mcp.types import TextContent, Tool | ||
|
||
from .base import BaseHandler | ||
|
||
logger = logging.getLogger("mcp-text-editor") | ||
|
||
|
||
class AppendTextFileContentsHandler(BaseHandler): | ||
"""Handler for appending content to an existing text file.""" | ||
|
||
name = "append_text_file_contents" | ||
description = "Append content to an existing text file. The file must exist." | ||
|
||
def get_tool_description(self) -> Tool: | ||
"""Get the tool description.""" | ||
return Tool( | ||
name=self.name, | ||
description=self.description, | ||
inputSchema={ | ||
"type": "object", | ||
"properties": { | ||
"file_path": { | ||
"type": "string", | ||
"description": "Path to the text file. File path must be absolute.", | ||
}, | ||
"contents": { | ||
"type": "string", | ||
"description": "Content to append to the file", | ||
}, | ||
"file_hash": { | ||
"type": "string", | ||
"description": "Hash of the file contents for concurrency control. it should be matched with the file_hash when get_text_file_contents is called.", | ||
}, | ||
"encoding": { | ||
"type": "string", | ||
"description": "Text encoding (default: 'utf-8')", | ||
"default": "utf-8", | ||
}, | ||
}, | ||
"required": ["file_path", "contents", "file_hash"], | ||
}, | ||
) | ||
|
||
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]: | ||
"""Execute the tool with given arguments.""" | ||
try: | ||
if "file_path" not in arguments: | ||
raise RuntimeError("Missing required argument: file_path") | ||
if "contents" not in arguments: | ||
raise RuntimeError("Missing required argument: contents") | ||
if "file_hash" not in arguments: | ||
raise RuntimeError("Missing required argument: file_hash") | ||
|
||
file_path = arguments["file_path"] | ||
if not os.path.isabs(file_path): | ||
raise RuntimeError(f"File path must be absolute: {file_path}") | ||
|
||
# Check if file exists | ||
if not os.path.exists(file_path): | ||
raise RuntimeError(f"File does not exist: {file_path}") | ||
|
||
encoding = arguments.get("encoding", "utf-8") | ||
|
||
# Check file contents and hash before modification | ||
# Get file information and verify hash | ||
content, _, _, current_hash, total_lines, _ = ( | ||
await self.editor.read_file_contents(file_path, encoding=encoding) | ||
) | ||
|
||
# Verify file hash | ||
if current_hash != arguments["file_hash"]: | ||
raise RuntimeError("File hash mismatch - file may have been modified") | ||
|
||
# Ensure the append content ends with newline | ||
append_content = arguments["contents"] | ||
if not append_content.endswith("\n"): | ||
append_content += "\n" | ||
|
||
# Create patch for append operation | ||
result = await self.editor.edit_file_contents( | ||
file_path, | ||
expected_hash=arguments["file_hash"], | ||
patches=[ | ||
{ | ||
"start": total_lines + 1, | ||
"end": None, | ||
"contents": append_content, | ||
"range_hash": "", | ||
} | ||
], | ||
encoding=encoding, | ||
) | ||
|
||
return [TextContent(type="text", text=json.dumps(result, indent=2))] | ||
|
||
except Exception as e: | ||
logger.error(f"Error processing request: {str(e)}") | ||
logger.error(traceback.format_exc()) | ||
raise RuntimeError(f"Error processing request: {str(e)}") from e |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
"""Base handler for MCP Text Editor.""" | ||
|
||
from typing import Any, Dict, Sequence | ||
|
||
from mcp.types import TextContent, Tool | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
"""Handler for creating new text files.""" | ||
|
||
import json | ||
import logging | ||
import os | ||
import traceback | ||
from typing import Any, Dict, Sequence | ||
|
||
from mcp.types import TextContent, Tool | ||
|
||
from .base import BaseHandler | ||
|
||
logger = logging.getLogger("mcp-text-editor") | ||
|
||
|
||
class CreateTextFileHandler(BaseHandler): | ||
"""Handler for creating a new text file.""" | ||
|
||
name = "create_text_file" | ||
description = ( | ||
"Create a new text file with given content. The file must not exist already." | ||
) | ||
|
||
def get_tool_description(self) -> Tool: | ||
"""Get the tool description.""" | ||
return Tool( | ||
name=self.name, | ||
description=self.description, | ||
inputSchema={ | ||
"type": "object", | ||
"properties": { | ||
"file_path": { | ||
"type": "string", | ||
"description": "Path to the text file. File path must be absolute.", | ||
}, | ||
"contents": { | ||
"type": "string", | ||
"description": "Content to write to the file", | ||
}, | ||
"encoding": { | ||
"type": "string", | ||
"description": "Text encoding (default: 'utf-8')", | ||
"default": "utf-8", | ||
}, | ||
}, | ||
"required": ["file_path", "contents"], | ||
}, | ||
) | ||
|
||
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]: | ||
"""Execute the tool with given arguments.""" | ||
try: | ||
if "file_path" not in arguments: | ||
raise RuntimeError("Missing required argument: file_path") | ||
if "contents" not in arguments: | ||
raise RuntimeError("Missing required argument: contents") | ||
|
||
file_path = arguments["file_path"] | ||
if not os.path.isabs(file_path): | ||
raise RuntimeError(f"File path must be absolute: {file_path}") | ||
|
||
# Check if file already exists | ||
if os.path.exists(file_path): | ||
raise RuntimeError(f"File already exists: {file_path}") | ||
|
||
encoding = arguments.get("encoding", "utf-8") | ||
|
||
# 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 | ||
patches=[ | ||
{ | ||
"start": 1, | ||
"end": None, | ||
"contents": arguments["contents"], | ||
"range_hash": "", # Empty range_hash for new file | ||
} | ||
], | ||
encoding=encoding, | ||
) | ||
return [TextContent(type="text", text=json.dumps(result, indent=2))] | ||
|
||
except Exception as e: | ||
logger.error(f"Error processing request: {str(e)}") | ||
logger.error(traceback.format_exc()) | ||
raise RuntimeError(f"Error processing request: {str(e)}") from e |
Oops, something went wrong.