Skip to content

Commit 0f12a65

Browse files
committed
Refactor Makefile and update dependencies: Replace pytest commands with uv run for better environment management. Upgrade mcp dependency to version 1.2.0 and change pytest's asyncio mode to 'auto' in pyproject.toml. Refactor server implementation to use FastMCP, streamline tool registration, and enhance test coverage for new text file manipulation tools.
1 parent 5ab454e commit 0f12a65

File tree

7 files changed

+157
-259
lines changed

7 files changed

+157
-259
lines changed

Makefile

+9-9
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,27 @@
22
.DEFAULT_GOAL := all
33

44
test:
5-
pytest
5+
uv run pytest
66

77
install:
88
uv sync --all-extras
99

1010
coverage:
11-
pytest --cov=mcp_text_editor --cov-report=term-missing
11+
uv run pytest --cov=mcp_text_editor --cov-report=term-missing
1212

1313
format:
14-
black src tests
15-
isort src tests
16-
ruff check --fix src tests
14+
uv run black src tests
15+
uv run isort src tests
16+
uv run ruff check --fix src tests
1717

1818

1919
lint:
20-
black --check src tests
21-
isort --check src tests
22-
ruff check src tests
20+
uv run black --check src tests
21+
uv run isort --check src tests
22+
uv run ruff check src tests
2323

2424
typecheck:
25-
mypy src tests
25+
uv run mypy src tests
2626

2727
# Run all checks required before pushing
2828
check: lint typecheck

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = [
77
]
88
dependencies = [
99
"asyncio>=3.4.3",
10-
"mcp>=1.1.2",
10+
"mcp>=1.2.0",
1111
"chardet>=5.2.0",
1212
]
1313
requires-python = ">=3.13"
@@ -38,7 +38,7 @@ requires = ["hatchling"]
3838
build-backend = "hatchling.build"
3939

4040
[tool.pytest.ini_options]
41-
asyncio_mode = "strict"
41+
asyncio_mode = "auto"
4242
testpaths = "tests"
4343
asyncio_default_fixture_loop_scope = "function"
4444
pythonpath = ["src"]

src/mcp_text_editor/handlers/base.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,18 @@
22

33
from typing import Any, Dict, Sequence
44

5-
from mcp.types import TextContent, Tool
5+
from mcp.types import TextContent
66

77
from ..text_editor import TextEditor
88

99

1010
class BaseHandler:
1111
"""Base class for handlers."""
1212

13-
name: str = ""
14-
description: str = ""
15-
1613
def __init__(self, editor: TextEditor | None = None):
1714
"""Initialize the handler."""
1815
self.editor = editor if editor is not None else TextEditor()
1916

20-
def get_tool_description(self) -> Tool:
21-
"""Get the tool description."""
22-
raise NotImplementedError
23-
2417
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
2518
"""Execute the tool with given arguments."""
2619
raise NotImplementedError

src/mcp_text_editor/server.py

+45-57
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""MCP Text Editor Server implementation."""
22

33
import logging
4-
import traceback
5-
from collections.abc import Sequence
6-
from typing import Any, List
4+
from typing import Sequence
75

8-
from mcp.server import Server
9-
from mcp.types import TextContent, Tool
6+
from mcp.server.fastmcp import FastMCP
7+
from mcp.types import TextContent
108

119
from .handlers import (
1210
AppendTextFileContentsHandler,
@@ -22,9 +20,9 @@
2220
logging.basicConfig(level=logging.INFO)
2321
logger = logging.getLogger("mcp-text-editor")
2422

25-
app = Server("mcp-text-editor")
23+
app = FastMCP("mcp-text-editor")
2624

27-
# Initialize tool handlers
25+
# Initialize handlers
2826
get_contents_handler = GetTextFileContentsHandler()
2927
patch_file_handler = PatchTextFileContentsHandler()
3028
create_file_handler = CreateTextFileHandler()
@@ -33,58 +31,48 @@
3331
insert_file_handler = InsertTextFileContentsHandler()
3432

3533

36-
@app.list_tools()
37-
async def list_tools() -> List[Tool]:
38-
"""List available tools."""
39-
return [
40-
get_contents_handler.get_tool_description(),
41-
create_file_handler.get_tool_description(),
42-
append_file_handler.get_tool_description(),
43-
delete_contents_handler.get_tool_description(),
44-
insert_file_handler.get_tool_description(),
45-
patch_file_handler.get_tool_description(),
46-
]
47-
48-
49-
@app.call_tool()
50-
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
51-
"""Handle tool calls."""
52-
logger.info(f"Calling tool: {name}")
53-
try:
54-
if name == get_contents_handler.name:
55-
return await get_contents_handler.run_tool(arguments)
56-
elif name == create_file_handler.name:
57-
return await create_file_handler.run_tool(arguments)
58-
elif name == append_file_handler.name:
59-
return await append_file_handler.run_tool(arguments)
60-
elif name == delete_contents_handler.name:
61-
return await delete_contents_handler.run_tool(arguments)
62-
elif name == insert_file_handler.name:
63-
return await insert_file_handler.run_tool(arguments)
64-
elif name == patch_file_handler.name:
65-
return await patch_file_handler.run_tool(arguments)
66-
else:
67-
raise ValueError(f"Unknown tool: {name}")
68-
except ValueError:
69-
logger.error(traceback.format_exc())
70-
raise
71-
except Exception as e:
72-
logger.error(traceback.format_exc())
73-
raise RuntimeError(f"Error executing command: {str(e)}") from e
34+
# Register tools
35+
@app.tool()
36+
async def get_text_file_contents(path: str) -> Sequence[TextContent]:
37+
"""Get the contents of a text file."""
38+
return await get_contents_handler.run_tool({"path": path})
39+
40+
41+
@app.tool()
42+
async def patch_text_file_contents(path: str, content: str) -> Sequence[TextContent]:
43+
"""Patch the contents of a text file."""
44+
return await patch_file_handler.run_tool({"path": path, "content": content})
45+
46+
47+
@app.tool()
48+
async def create_text_file(path: str) -> Sequence[TextContent]:
49+
"""Create a new text file."""
50+
return await create_file_handler.run_tool({"path": path})
51+
52+
53+
@app.tool()
54+
async def append_text_file_contents(path: str, content: str) -> Sequence[TextContent]:
55+
"""Append content to a text file."""
56+
return await append_file_handler.run_tool({"path": path, "content": content})
57+
58+
59+
@app.tool()
60+
async def delete_text_file_contents(path: str) -> Sequence[TextContent]:
61+
"""Delete the contents of a text file."""
62+
return await delete_contents_handler.run_tool({"path": path})
63+
64+
65+
@app.tool()
66+
async def insert_text_file_contents(
67+
path: str, content: str, position: int
68+
) -> Sequence[TextContent]:
69+
"""Insert content into a text file at a specific position."""
70+
return await insert_file_handler.run_tool(
71+
{"path": path, "content": content, "position": position}
72+
)
7473

7574

7675
async def main() -> None:
7776
"""Main entry point for the MCP text editor server."""
7877
logger.info(f"Starting MCP text editor server v{__version__}")
79-
try:
80-
from mcp.server.stdio import stdio_server
81-
82-
async with stdio_server() as (read_stream, write_stream):
83-
await app.run(
84-
read_stream,
85-
write_stream,
86-
app.create_initialization_options(),
87-
)
88-
except Exception as e:
89-
logger.error(f"Server error: {str(e)}")
90-
raise
78+
await app.run() # type: ignore[func-returns-value]

tests/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88
import pytest_asyncio
9-
from mcp.server import Server
9+
from mcp.server.fastmcp import FastMCP
1010

1111
from mcp_text_editor.server import app
1212

@@ -58,7 +58,7 @@ async def drain(self) -> None:
5858

5959

6060
@pytest_asyncio.fixture
61-
async def mock_server() -> AsyncGenerator[tuple[Server, MockStream], None]:
61+
async def mock_server() -> AsyncGenerator[tuple[FastMCP, MockStream], None]:
6262
"""Create a mock server for testing."""
6363
mock_write_stream = MockStream()
6464
yield app, mock_write_stream

0 commit comments

Comments
 (0)