fix: address PR review feedback (round 8)
- Fix inconsistent error code in set_collective_emoji (400 → -32603) - Allow clearing emoji via set_collective_emoji(emoji=None) - Remove destructiveHint from trash operations (soft deletes are recoverable) - Change delete_collective to idempotentHint=False (requires trash precondition) - Add restore_collective and get_trashed_collectives tools - Add unit tests for ValueError guard, clear-emoji path, and new tools - Add integration test for full trash/restore/delete lifecycle - Verify move_page returns new title in response message Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7224a2ebe3
commit
cb16060b1b
@@ -118,6 +118,26 @@ class CollectivesClient(BaseNextcloudClient):
|
||||
)
|
||||
self._unwrap_ocs(response.json())
|
||||
|
||||
# Trash (collectives)
|
||||
|
||||
async def get_trashed_collectives(self) -> list[dict[str, Any]]:
|
||||
"""List trashed collectives."""
|
||||
response = await self._make_request(
|
||||
"GET", f"{API_BASE}/collectives/trash", headers=self._OCS_HEADERS
|
||||
)
|
||||
data = self._unwrap_ocs(response.json())
|
||||
return data["collectives"]
|
||||
|
||||
async def restore_collective(self, collective_id: int) -> dict[str, Any]:
|
||||
"""Restore a collective from trash."""
|
||||
response = await self._make_request(
|
||||
"PATCH",
|
||||
f"{API_BASE}/collectives/trash/{collective_id}",
|
||||
headers=self._OCS_HEADERS,
|
||||
)
|
||||
data = self._unwrap_ocs(response.json())
|
||||
return data["collective"]
|
||||
|
||||
# Pages
|
||||
|
||||
async def get_pages(self, collective_id: int) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -140,6 +140,13 @@ class ListTrashedPagesResponse(ListPagesResponse):
|
||||
)
|
||||
|
||||
|
||||
class ListTrashedCollectivesResponse(BaseResponse):
|
||||
"""Response for listing trashed collectives."""
|
||||
|
||||
collectives: list[Collective] = Field(description="List of trashed collectives")
|
||||
total: int = Field(description="Total number of trashed collectives")
|
||||
|
||||
|
||||
class ListTagsResponse(BaseResponse):
|
||||
"""Response for listing tags in a collective."""
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from nextcloud_mcp_server.models.collectives import (
|
||||
ListCollectivesResponse,
|
||||
ListPagesResponse,
|
||||
ListTagsResponse,
|
||||
ListTrashedCollectivesResponse,
|
||||
ListTrashedPagesResponse,
|
||||
PageInfo,
|
||||
PageOperationResponse,
|
||||
@@ -241,21 +242,22 @@ def configure_collectives_tools(mcp: FastMCP):
|
||||
@require_scopes("collectives:write")
|
||||
@instrument_tool
|
||||
async def collectives_set_collective_emoji(
|
||||
ctx: Context, collective_id: int, emoji: str
|
||||
ctx: Context, collective_id: int, emoji: str | None = None
|
||||
) -> CollectiveOperationResponse:
|
||||
"""Set the emoji on a Nextcloud Collective.
|
||||
"""Set or clear the emoji on a Nextcloud Collective.
|
||||
|
||||
Setting the same emoji twice produces the same result (idempotent).
|
||||
Pass emoji=None to clear the emoji.
|
||||
|
||||
Args:
|
||||
collective_id: ID of the collective
|
||||
emoji: Emoji to set on the collective
|
||||
emoji: Emoji to set, or None to clear
|
||||
"""
|
||||
client = await get_client(ctx)
|
||||
try:
|
||||
raw = await client.collectives.update_collective(collective_id, emoji)
|
||||
except ValueError as e:
|
||||
raise McpError(ErrorData(code=400, message=str(e))) from e
|
||||
raise McpError(ErrorData(code=-32603, message=str(e))) from e
|
||||
except (OCSError, HTTPStatusError) as e:
|
||||
raise _handle_collectives_error(e) from e
|
||||
collective = Collective(**raw)
|
||||
@@ -267,9 +269,7 @@ def configure_collectives_tools(mcp: FastMCP):
|
||||
|
||||
@mcp.tool(
|
||||
title="Trash Collective",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=False, openWorldHint=True
|
||||
),
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("collectives:write")
|
||||
@instrument_tool
|
||||
@@ -297,7 +297,7 @@ def configure_collectives_tools(mcp: FastMCP):
|
||||
@mcp.tool(
|
||||
title="Delete Collective",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
destructiveHint=True, idempotentHint=False, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("collectives:write")
|
||||
@@ -325,6 +325,56 @@ def configure_collectives_tools(mcp: FastMCP):
|
||||
message="Collective permanently deleted",
|
||||
)
|
||||
|
||||
@mcp.tool(
|
||||
title="List Trashed Collectives",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("collectives:read")
|
||||
@instrument_tool
|
||||
async def collectives_get_trashed_collectives(
|
||||
ctx: Context,
|
||||
) -> ListTrashedCollectivesResponse:
|
||||
"""List all trashed Nextcloud Collectives.
|
||||
|
||||
Returns collectives that have been soft-deleted and can be restored
|
||||
or permanently deleted.
|
||||
"""
|
||||
client = await get_client(ctx)
|
||||
try:
|
||||
raw = await client.collectives.get_trashed_collectives()
|
||||
except (OCSError, HTTPStatusError) as e:
|
||||
raise _handle_collectives_error(e) from e
|
||||
collectives = [Collective(**c) for c in raw]
|
||||
return ListTrashedCollectivesResponse(
|
||||
collectives=collectives, total=len(collectives)
|
||||
)
|
||||
|
||||
@mcp.tool(
|
||||
title="Restore Collective",
|
||||
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("collectives:write")
|
||||
@instrument_tool
|
||||
async def collectives_restore_collective(
|
||||
ctx: Context, collective_id: int
|
||||
) -> CollectiveOperationResponse:
|
||||
"""Restore a Nextcloud Collective from trash.
|
||||
|
||||
Args:
|
||||
collective_id: ID of the trashed collective to restore
|
||||
"""
|
||||
client = await get_client(ctx)
|
||||
try:
|
||||
raw = await client.collectives.restore_collective(collective_id)
|
||||
except (OCSError, HTTPStatusError) as e:
|
||||
raise _handle_collectives_error(e) from e
|
||||
collective = Collective(**raw)
|
||||
return CollectiveOperationResponse(
|
||||
collective_id=collective.id,
|
||||
status_code=200,
|
||||
message=f"Collective '{collective.name}' restored from trash",
|
||||
)
|
||||
|
||||
@mcp.tool(
|
||||
title="Create Collective Page",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
@@ -401,9 +451,7 @@ def configure_collectives_tools(mcp: FastMCP):
|
||||
|
||||
@mcp.tool(
|
||||
title="Trash Collective Page",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=False, openWorldHint=True
|
||||
),
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("collectives:write")
|
||||
@instrument_tool
|
||||
|
||||
Reference in New Issue
Block a user