Skip to content

Commit

Permalink
Refactor Makefile and update dependencies: Replace pytest commands wi…
Browse files Browse the repository at this point in the history
…th 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.
  • Loading branch information
tumf committed Jan 20, 2025
1 parent 5ab454e commit 0f12a65
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 259 deletions.
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
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
102 changes: 45 additions & 57 deletions src/mcp_text_editor/server.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
"""MCP Text Editor Server implementation."""

import logging
import traceback
from collections.abc import Sequence
from typing import Any, List
from typing import Sequence

from mcp.server import Server
from mcp.types import TextContent, Tool
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent

from .handlers import (
AppendTextFileContentsHandler,
Expand All @@ -22,9 +20,9 @@
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp-text-editor")

app = Server("mcp-text-editor")
app = FastMCP("mcp-text-editor")

# Initialize tool handlers
# Initialize handlers
get_contents_handler = GetTextFileContentsHandler()
patch_file_handler = PatchTextFileContentsHandler()
create_file_handler = CreateTextFileHandler()
Expand All @@ -33,58 +31,48 @@
insert_file_handler = InsertTextFileContentsHandler()


@app.list_tools()
async def list_tools() -> List[Tool]:
"""List available tools."""
return [
get_contents_handler.get_tool_description(),
create_file_handler.get_tool_description(),
append_file_handler.get_tool_description(),
delete_contents_handler.get_tool_description(),
insert_file_handler.get_tool_description(),
patch_file_handler.get_tool_description(),
]


@app.call_tool()
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
"""Handle tool calls."""
logger.info(f"Calling tool: {name}")
try:
if name == get_contents_handler.name:
return await get_contents_handler.run_tool(arguments)
elif name == create_file_handler.name:
return await create_file_handler.run_tool(arguments)
elif name == append_file_handler.name:
return await append_file_handler.run_tool(arguments)
elif name == delete_contents_handler.name:
return await delete_contents_handler.run_tool(arguments)
elif name == insert_file_handler.name:
return await insert_file_handler.run_tool(arguments)
elif name == patch_file_handler.name:
return await patch_file_handler.run_tool(arguments)
else:
raise ValueError(f"Unknown tool: {name}")
except ValueError:
logger.error(traceback.format_exc())
raise
except Exception as e:
logger.error(traceback.format_exc())
raise RuntimeError(f"Error executing command: {str(e)}") from e
# Register tools
@app.tool()
async def get_text_file_contents(path: str) -> Sequence[TextContent]:
"""Get the contents of a text file."""
return await get_contents_handler.run_tool({"path": path})


@app.tool()
async def patch_text_file_contents(path: str, content: str) -> Sequence[TextContent]:
"""Patch the contents of a text file."""
return await patch_file_handler.run_tool({"path": path, "content": content})


@app.tool()
async def create_text_file(path: str) -> Sequence[TextContent]:
"""Create a new text file."""
return await create_file_handler.run_tool({"path": path})


@app.tool()
async def append_text_file_contents(path: str, content: str) -> Sequence[TextContent]:
"""Append content to a text file."""
return await append_file_handler.run_tool({"path": path, "content": content})


@app.tool()
async def delete_text_file_contents(path: str) -> Sequence[TextContent]:
"""Delete the contents of a text file."""
return await delete_contents_handler.run_tool({"path": path})


@app.tool()
async def insert_text_file_contents(
path: str, content: str, position: int
) -> Sequence[TextContent]:
"""Insert content into a text file at a specific position."""
return await insert_file_handler.run_tool(
{"path": path, "content": content, "position": position}
)


async def main() -> None:
"""Main entry point for the MCP text editor server."""
logger.info(f"Starting MCP text editor server v{__version__}")
try:
from mcp.server.stdio import stdio_server

async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options(),
)
except Exception as e:
logger.error(f"Server error: {str(e)}")
raise
await app.run() # type: ignore[func-returns-value]
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest
import pytest_asyncio
from mcp.server import Server
from mcp.server.fastmcp import FastMCP

from mcp_text_editor.server import app

Expand Down Expand Up @@ -58,7 +58,7 @@ async def drain(self) -> None:


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

0 comments on commit 0f12a65

Please sign in to comment.