Skip to content
Merged
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
25 changes: 13 additions & 12 deletions src/basic_memory/mcp/tools/edit_note.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Edit note tool for Basic Memory MCP server."""

from typing import TYPE_CHECKING, Annotated, Optional, Literal
from typing import TYPE_CHECKING, Annotated, Literal, Optional

import logfire
from httpx import HTTPStatusError
Expand Down Expand Up @@ -31,6 +31,15 @@
)
from basic_memory.utils import normalize_project_reference, validate_project_path

EDIT_OPERATIONS = (
"append",
"prepend",
"find_replace",
"replace_section",
"insert_before_section",
"insert_after_section",
)


def _parse_identifier_to_title_and_directory(identifier: str) -> tuple[str, str]:
"""Parse an identifier into (title, directory) for creating a new note.
Expand Down Expand Up @@ -318,7 +327,7 @@ def _format_error_response(
)
async def edit_note(
identifier: str,
operation: str,
operation: Annotated[str, Field(json_schema_extra={"enum": list(EDIT_OPERATIONS)})],
# Accept common replacement-content aliases. Models trained on diff/patch
# APIs reach for new_content/replacement/replace_with on first try.
content: Annotated[
Expand Down Expand Up @@ -512,17 +521,9 @@ async def edit_note(
)

# Validate operation
valid_operations = [
"append",
"prepend",
"find_replace",
"replace_section",
"insert_before_section",
"insert_after_section",
]
if operation not in valid_operations:
if operation not in EDIT_OPERATIONS:
raise ValueError(
f"Invalid operation '{operation}'. Must be one of: {', '.join(valid_operations)}"
f"Invalid operation '{operation}'. Must be one of: {', '.join(EDIT_OPERATIONS)}"
)

# Validate required parameters for specific operations
Expand Down
22 changes: 22 additions & 0 deletions tests/mcp/test_tool_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@
"search_notes_ui": {"readOnlyHint": True, "destructiveHint": False},
}

EXPECTED_EDIT_NOTE_OPERATIONS = [
"append",
"prepend",
"find_replace",
"replace_section",
"insert_before_section",
"insert_after_section",
]


TOOL_FUNCTIONS: dict[str, object] = {
"build_context": tools.build_context,
Expand Down Expand Up @@ -250,6 +259,19 @@ async def test_mcp_tool_annotations_meet_directory_requirements():
)


@pytest.mark.asyncio
async def test_edit_note_operation_schema_exposes_supported_operations():
"""The edit operation is a fixed choice set, not a free-form string."""
tool_list = await mcp.list_tools()
edit_note_tool = next(tool for tool in tool_list if tool.name == "edit_note")

input_schema = edit_note_tool.to_mcp_tool().inputSchema
operation_schema = input_schema["properties"]["operation"]

assert operation_schema["type"] == "string"
assert operation_schema["enum"] == EXPECTED_EDIT_NOTE_OPERATIONS


@pytest.mark.asyncio
async def test_mcp_tools_have_title_and_tags():
"""Every registered MCP tool must declare a human-readable title and at least one tag.
Expand Down
Loading