feat(notes): Add append to note functionality

This commit is contained in:
Chris Coutinho
2025-05-24 21:28:10 +02:00
parent dd7eab05db
commit 892e0b4c01
4 changed files with 184 additions and 21 deletions
+1
View File
@@ -12,6 +12,7 @@ Currently, the server primarily interacts with the Nextcloud Notes API, providin
* `nc_notes_create_note`: Create a new note. * `nc_notes_create_note`: Create a new note.
* `nc_notes_update_note`: Update an existing note by ID. * `nc_notes_update_note`: Update an existing note by ID.
* `nc_notes_append_content`: Append content to an existing note with a clear separator.
* `nc_notes_delete_note`: Delete a note by ID. * `nc_notes_delete_note`: Delete a note by ID.
* `nc_notes_search_notes`: Search notes by title or content. * `nc_notes_search_notes`: Search notes by title or content.
* `nc_get_note`: Get a specific note by ID. * `nc_get_note`: Get a specific note by ID.
+31 -3
View File
@@ -162,6 +162,34 @@ class NextcloudClient:
return updated_note return updated_note
def notes_append_content(self, *, note_id: int, content: str):
"""Append content to an existing note with a standard separator"""
logger.info(f"Appending content to note {note_id}")
# Get current note
current_note = self.notes_get_note(note_id=note_id)
# Use fixed separator for consistency
separator = "\n---\n"
# Combine content
existing_content = current_note.get("content", "")
if existing_content:
new_content = existing_content + separator + content
else:
new_content = content # No separator needed for empty notes
logger.info(f"Combining existing content ({len(existing_content)} chars) with new content ({len(content)} chars)")
# Update with combined content
return self.notes_update_note(
note_id=note_id,
etag=current_note["etag"],
content=new_content,
title=None, # Keep existing title
category=None # Keep existing category
)
def notes_search_notes(self, *, query: str): def notes_search_notes(self, *, query: str):
""" """
Search notes using token-based matching with relevance ranking. Search notes using token-based matching with relevance ranking.
@@ -308,7 +336,7 @@ class NextcloudClient:
return {"status_code": response.status_code} return {"status_code": response.status_code}
except HTTPStatusError as e: except HTTPStatusError as e:
logger.error( logger.warning(
"HTTP error deleting WebDAV resource '%s': %s", "HTTP error deleting WebDAV resource '%s': %s",
webdav_path, webdav_path,
e, e,
@@ -321,7 +349,7 @@ class NextcloudClient:
logger.info("Resource '%s' not found, no deletion needed.", webdav_path) logger.info("Resource '%s' not found, no deletion needed.", webdav_path)
return {"status_code": 404} # Indicate resource was not found return {"status_code": 404} # Indicate resource was not found
except Exception as e: except Exception as e:
logger.error( logger.warning(
"Unexpected error deleting WebDAV resource '%s': %s", "Unexpected error deleting WebDAV resource '%s': %s",
webdav_path, webdav_path,
e, e,
@@ -380,7 +408,7 @@ class NextcloudClient:
except Exception as e: except Exception as e:
# Log the error but don't re-raise, as API note deletion itself was successful # Log the error but don't re-raise, as API note deletion itself was successful
# Also, we want to try other potential categories even if one fails # Also, we want to try other potential categories even if one fails
logger.error(f"Failed during WebDAV deletion for category '{cat}' attachment directory: {e}") logger.warning(f"Failed during WebDAV deletion for category '{cat}' attachment directory: {e}")
return json_response return json_response
+8
View File
@@ -98,6 +98,14 @@ def nc_notes_update_note(
) )
@mcp.tool()
def nc_notes_append_content(note_id: int, content: str, ctx: Context):
"""Append content to an existing note with a clear separator"""
logger.info("Appending content to note %s", note_id)
client: NextcloudClient = ctx.request_context.lifespan_context.client
return client.notes_append_content(note_id=note_id, content=content)
@mcp.tool() @mcp.tool()
def nc_notes_search_notes(query: str, ctx: Context): def nc_notes_search_notes(query: str, ctx: Context):
"""Search notes by title or content, returning only id, title, and category.""" """Search notes by title or content, returning only id, title, and category."""
+126
View File
@@ -111,4 +111,130 @@ def test_notes_api_delete_nonexistent(nc_client: NextcloudClient):
assert excinfo.value.response.status_code == 404 assert excinfo.value.response.status_code == 404
logger.info(f"Deleting non-existent note ID: {non_existent_id} correctly failed with 404.") logger.info(f"Deleting non-existent note ID: {non_existent_id} correctly failed with 404.")
def test_notes_api_append_content_to_existing_note(nc_client: NextcloudClient, temporary_note: dict):
"""
Tests appending content to an existing note using the new append functionality.
"""
created_note_data = temporary_note
note_id = created_note_data["id"]
original_content = created_note_data["content"]
append_text = f"Appended content {uuid.uuid4().hex[:8]}"
logger.info(f"Appending content to note ID: {note_id}")
updated_note = nc_client.notes_append_content(
note_id=note_id,
content=append_text
)
logger.info(f"Note after append: {updated_note}")
# Verify the note was updated
assert updated_note["id"] == note_id
assert "etag" in updated_note
assert updated_note["etag"] != created_note_data["etag"] # Etag must change
# Verify content has the separator and appended text
expected_content = original_content + "\n---\n" + append_text
assert updated_note["content"] == expected_content
# Verify by reading the note again
time.sleep(1) # Allow potential propagation delay
read_note = nc_client.notes_get_note(note_id=note_id)
assert read_note["content"] == expected_content
logger.info(f"Successfully appended content to note ID: {note_id}")
def test_notes_api_append_content_to_empty_note(nc_client: NextcloudClient):
"""
Tests appending content to an empty note (no separator should be added).
"""
# Create an empty note
test_title = f"Empty Note {uuid.uuid4().hex[:8]}"
test_category = "Test"
logger.info(f"Creating empty note for append test")
empty_note = nc_client.notes_create_note(
title=test_title,
content="", # Empty content
category=test_category
)
note_id = empty_note["id"]
try:
append_text = f"First content {uuid.uuid4().hex[:8]}"
logger.info(f"Appending content to empty note ID: {note_id}")
updated_note = nc_client.notes_append_content(
note_id=note_id,
content=append_text
)
# For empty notes, content should just be the appended text (no separator)
assert updated_note["content"] == append_text
# Verify by reading the note again
time.sleep(1)
read_note = nc_client.notes_get_note(note_id=note_id)
assert read_note["content"] == append_text
logger.info(f"Successfully appended content to empty note ID: {note_id}")
finally:
# Clean up the test note
try:
nc_client.notes_delete_note(note_id=note_id)
logger.info(f"Cleaned up test note ID: {note_id}")
except Exception as e:
logger.warning(f"Failed to clean up test note ID: {note_id}: {e}")
def test_notes_api_append_content_multiple_times(nc_client: NextcloudClient, temporary_note: dict):
"""
Tests appending content multiple times to verify separator behavior.
"""
created_note_data = temporary_note
note_id = created_note_data["id"]
original_content = created_note_data["content"]
first_append = f"First append {uuid.uuid4().hex[:8]}"
second_append = f"Second append {uuid.uuid4().hex[:8]}"
logger.info(f"Performing multiple appends to note ID: {note_id}")
# First append
updated_note = nc_client.notes_append_content(
note_id=note_id,
content=first_append
)
expected_content_after_first = original_content + "\n---\n" + first_append
assert updated_note["content"] == expected_content_after_first
# Second append
updated_note = nc_client.notes_append_content(
note_id=note_id,
content=second_append
)
expected_content_after_second = expected_content_after_first + "\n---\n" + second_append
assert updated_note["content"] == expected_content_after_second
# Verify by reading the note again
time.sleep(1)
read_note = nc_client.notes_get_note(note_id=note_id)
assert read_note["content"] == expected_content_after_second
logger.info(f"Successfully performed multiple appends to note ID: {note_id}")
def test_notes_api_append_content_nonexistent_note(nc_client: NextcloudClient):
"""
Tests that appending to a non-existent note fails with 404.
"""
non_existent_id = 999999999
logger.info(f"Attempting to append to non-existent note ID: {non_existent_id}")
with pytest.raises(HTTPStatusError) as excinfo:
nc_client.notes_append_content(
note_id=non_existent_id,
content="This should fail"
)
assert excinfo.value.response.status_code == 404
logger.info(f"Appending to non-existent note ID: {non_existent_id} correctly failed with 404.")
# --- Attachment tests moved to test_attachments.py --- # --- Attachment tests moved to test_attachments.py ---