fix: address PR review feedback (round 7) and fix CI

- Rename collectives_update_collective to collectives_set_collective_emoji
  (more precise since only emoji is settable)
- Use standard JSON-RPC error code -32603 (INTERNAL_ERROR) instead of -1
- Handle UnicodeDecodeError when reading page content via WebDAV
- Replace brittle 'Welcome' content assertion with length check

Fixes CI: test_update_operations_not_idempotent no longer matches the
renamed tool, which is correctly idempotent (no ETag involved).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-03-26 23:18:42 +01:00
co-authored by Claude Opus 4.6
parent 85119bde91
commit 7224a2ebe3
2 changed files with 15 additions and 15 deletions
+10 -10
View File
@@ -34,8 +34,8 @@ logger = logging.getLogger(__name__)
def _handle_collectives_error(e: OCSError | HTTPStatusError) -> McpError: def _handle_collectives_error(e: OCSError | HTTPStatusError) -> McpError:
"""Convert OCS or HTTP errors to McpError.""" """Convert OCS or HTTP errors to McpError."""
if isinstance(e, OCSError): if isinstance(e, OCSError):
return McpError(ErrorData(code=-1, message=e.message)) return McpError(ErrorData(code=-32603, message=e.message))
return McpError(ErrorData(code=-1, message=str(e))) return McpError(ErrorData(code=-32603, message=str(e)))
def configure_collectives_tools(mcp: FastMCP): def configure_collectives_tools(mcp: FastMCP):
@@ -124,7 +124,7 @@ def configure_collectives_tools(mcp: FastMCP):
try: try:
file_bytes, _ = await client.webdav.read_file(webdav_path) file_bytes, _ = await client.webdav.read_file(webdav_path)
content = file_bytes.decode("utf-8") content = file_bytes.decode("utf-8")
except (HTTPStatusError, OSError) as e: except (HTTPStatusError, OSError, UnicodeDecodeError) as e:
logger.warning( logger.warning(
"Failed to read page content via WebDAV: %s: %s", "Failed to read page content via WebDAV: %s: %s",
webdav_path, webdav_path,
@@ -235,21 +235,21 @@ def configure_collectives_tools(mcp: FastMCP):
) )
@mcp.tool( @mcp.tool(
title="Update Collective", title="Set Collective Emoji",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True), annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
) )
@require_scopes("collectives:write") @require_scopes("collectives:write")
@instrument_tool @instrument_tool
async def collectives_update_collective( async def collectives_set_collective_emoji(
ctx: Context, collective_id: int, emoji: str | None = None ctx: Context, collective_id: int, emoji: str
) -> CollectiveOperationResponse: ) -> CollectiveOperationResponse:
"""Update a Nextcloud Collective (emoji). """Set the emoji on a Nextcloud Collective.
At least one field must be provided. Setting the same emoji twice produces the same result (idempotent).
Args: Args:
collective_id: ID of the collective collective_id: ID of the collective
emoji: New emoji for the collective emoji: Emoji to set on the collective
""" """
client = await get_client(ctx) client = await get_client(ctx)
try: try:
@@ -262,7 +262,7 @@ def configure_collectives_tools(mcp: FastMCP):
return CollectiveOperationResponse( return CollectiveOperationResponse(
collective_id=collective.id, collective_id=collective.id,
status_code=200, status_code=200,
message=f"Collective updated (emoji: {collective.emoji})", message=f"Collective emoji set to: {collective.emoji}",
) )
@mcp.tool( @mcp.tool(
+5 -5
View File
@@ -71,7 +71,7 @@ async def test_collectives_tools_available(nc_mcp_client: ClientSession):
expected_tools = [ expected_tools = [
"collectives_get_collectives", "collectives_get_collectives",
"collectives_create_collective", "collectives_create_collective",
"collectives_update_collective", "collectives_set_collective_emoji",
"collectives_trash_collective", "collectives_trash_collective",
"collectives_delete_collective", "collectives_delete_collective",
"collectives_get_pages", "collectives_get_pages",
@@ -116,12 +116,12 @@ async def test_collectives_list(
logger.info(f"Found {data['total']} collectives") logger.info(f"Found {data['total']} collectives")
async def test_collectives_update_emoji( async def test_collectives_set_collective_emoji(
nc_mcp_client: ClientSession, temporary_collective: dict nc_mcp_client: ClientSession, temporary_collective: dict
): ):
"""Test updating a collective's emoji.""" """Test setting a collective's emoji."""
result = await nc_mcp_client.call_tool( result = await nc_mcp_client.call_tool(
"collectives_update_collective", "collectives_set_collective_emoji",
{"collective_id": temporary_collective["id"], "emoji": "📖"}, {"collective_id": temporary_collective["id"], "emoji": "📖"},
) )
assert result.isError is False assert result.isError is False
@@ -242,7 +242,7 @@ async def test_collectives_get_landing_page_content(
assert data["content"] is not None, ( assert data["content"] is not None, (
"Landing page should have auto-generated content" "Landing page should have auto-generated content"
) )
assert "Welcome" in data["content"], "Landing page should contain welcome text" assert len(data["content"]) > 0, "Landing page should have non-empty content"
logger.info(f"Landing page content: {len(data['content'])} bytes") logger.info(f"Landing page content: {len(data['content'])} bytes")