feat: add MCP tool annotations for enhanced UX

Add ToolAnnotations to all 105+ MCP tools across 13 modules to enable
better client-side UX with human-readable titles and behavioral hints.

Changes:
- Add title and ToolAnnotations to all @mcp.tool() decorators
- Apply correct idempotency classification per ADR-017
- Add destructiveHint for delete operations
- Set openWorldHint=False for semantic search (internal data only)

Modules updated:
- OAuth (4 tools): Authentication and provisioning
- Notes (7 tools): Note management
- WebDAV (11 tools): File operations
- Semantic (3 tools): Semantic search and RAG
- Calendar (16 tools): Events and todos
- Contacts (7 tools): Address book management
- Sharing (5 tools): File/folder sharing
- Tables (6 tools): Structured data
- Deck (25 tools): Kanban board management
- Cookbook (13 tools): Recipe management
- News (8 tools): RSS feed reader

Annotation patterns:
- Read operations: readOnlyHint=True, openWorldHint=True
- Create operations: idempotentHint=False, openWorldHint=True
- Update operations: idempotentHint=False, openWorldHint=True
- Delete operations: destructiveHint=True, idempotentHint=True, openWorldHint=True

See docs/ADR-017-mcp-tool-annotations.md for rationale and implementation details.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-12-11 12:45:02 +01:00
parent b9c94dfab0
commit e1412320a7
12 changed files with 1013 additions and 104 deletions
+69 -16
View File
@@ -3,6 +3,7 @@ import logging
from typing import Optional
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -19,7 +20,10 @@ logger = logging.getLogger(__name__)
def configure_calendar_tools(mcp: FastMCP):
# Calendar tools
@mcp.tool()
@mcp.tool(
title="List Calendars",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@instrument_tool
async def nc_calendar_list_calendars(ctx: Context) -> ListCalendarsResponse:
@@ -30,7 +34,10 @@ def configure_calendar_tools(mcp: FastMCP):
calendars = [Calendar(**cal_data) for cal_data in calendars_data]
return ListCalendarsResponse(calendars=calendars, total_count=len(calendars))
@mcp.tool()
@mcp.tool(
title="Create Calendar Event",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@instrument_tool
async def nc_calendar_create_event(
@@ -107,7 +114,10 @@ def configure_calendar_tools(mcp: FastMCP):
return await client.calendar.create_event(calendar_name, event_data)
@mcp.tool()
@mcp.tool(
title="List Calendar Events",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@instrument_tool
async def nc_calendar_list_events(
@@ -210,7 +220,10 @@ def configure_calendar_tools(mcp: FastMCP):
return events
@mcp.tool()
@mcp.tool(
title="Get Calendar Event",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@instrument_tool
async def nc_calendar_get_event(
@@ -223,7 +236,10 @@ def configure_calendar_tools(mcp: FastMCP):
event_data, etag = await client.calendar.get_event(calendar_name, event_uid)
return event_data
@mcp.tool()
@mcp.tool(
title="Update Calendar Event",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@instrument_tool
async def nc_calendar_update_event(
@@ -297,7 +313,12 @@ def configure_calendar_tools(mcp: FastMCP):
calendar_name, event_uid, event_data, etag
)
@mcp.tool()
@mcp.tool(
title="Delete Calendar Event",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("calendar:write")
@instrument_tool
async def nc_calendar_delete_event(
@@ -309,7 +330,10 @@ def configure_calendar_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.calendar.delete_event(calendar_name, event_uid)
@mcp.tool()
@mcp.tool(
title="Create Meeting",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@instrument_tool
async def nc_calendar_create_meeting(
@@ -376,7 +400,10 @@ def configure_calendar_tools(mcp: FastMCP):
return await client.calendar.create_event(calendar_name, event_data)
@mcp.tool()
@mcp.tool(
title="Get Upcoming Events",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@instrument_tool
async def nc_calendar_get_upcoming_events(
@@ -427,7 +454,10 @@ def configure_calendar_tools(mcp: FastMCP):
all_events.sort(key=lambda x: x.get("start_datetime", ""))
return all_events[:limit]
@mcp.tool()
@mcp.tool(
title="Find Availability",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@instrument_tool
async def nc_calendar_find_availability(
@@ -508,7 +538,10 @@ def configure_calendar_tools(mcp: FastMCP):
constraints=constraints,
)
@mcp.tool()
@mcp.tool(
title="Bulk Calendar Operations",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@instrument_tool
async def nc_calendar_bulk_operations(
@@ -758,7 +791,10 @@ def configure_calendar_tools(mcp: FastMCP):
"results": results,
}
@mcp.tool()
@mcp.tool(
title="Manage Calendar",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@instrument_tool
async def nc_calendar_manage_calendar(
@@ -828,7 +864,10 @@ def configure_calendar_tools(mcp: FastMCP):
# ============= Todo/Task Tools =============
@mcp.tool()
@mcp.tool(
title="List Todo Tasks",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("todo:read", "calendar:read")
@instrument_tool
async def nc_calendar_list_todos(
@@ -874,7 +913,10 @@ def configure_calendar_tools(mcp: FastMCP):
todos=todos, calendar_name=calendar_name, total_count=len(todos)
)
@mcp.tool()
@mcp.tool(
title="Create Todo Task",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("todo:write", "calendar:read")
@instrument_tool
async def nc_calendar_create_todo(
@@ -918,7 +960,10 @@ def configure_calendar_tools(mcp: FastMCP):
return await client.calendar.create_todo(calendar_name, todo_data)
@mcp.tool()
@mcp.tool(
title="Update Todo Task",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("todo:write", "calendar:read")
@instrument_tool
async def nc_calendar_update_todo(
@@ -979,7 +1024,12 @@ def configure_calendar_tools(mcp: FastMCP):
return await client.calendar.update_todo(calendar_name, todo_uid, todo_data)
@mcp.tool()
@mcp.tool(
title="Delete Todo Task",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("todo:write", "calendar:read")
@instrument_tool
async def nc_calendar_delete_todo(
@@ -1000,7 +1050,10 @@ def configure_calendar_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.calendar.delete_todo(calendar_name, todo_uid)
@mcp.tool()
@mcp.tool(
title="Search Todo Tasks",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("todo:read", "calendar:read")
@instrument_tool
async def nc_calendar_search_todos(
+33 -7
View File
@@ -1,6 +1,7 @@
import logging
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -11,7 +12,10 @@ logger = logging.getLogger(__name__)
def configure_contacts_tools(mcp: FastMCP):
# Contacts tools
@mcp.tool()
@mcp.tool(
title="List Address Books",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("contacts:read")
@instrument_tool
async def nc_contacts_list_addressbooks(ctx: Context):
@@ -19,7 +23,10 @@ def configure_contacts_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.contacts.list_addressbooks()
@mcp.tool()
@mcp.tool(
title="List Contacts",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("contacts:read")
@instrument_tool
async def nc_contacts_list_contacts(ctx: Context, *, addressbook: str):
@@ -27,7 +34,10 @@ def configure_contacts_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.contacts.list_contacts(addressbook=addressbook)
@mcp.tool()
@mcp.tool(
title="Create Address Book",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("contacts:write")
@instrument_tool
async def nc_contacts_create_addressbook(
@@ -44,7 +54,12 @@ def configure_contacts_tools(mcp: FastMCP):
name=name, display_name=display_name
)
@mcp.tool()
@mcp.tool(
title="Delete Address Book",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("contacts:write")
@instrument_tool
async def nc_contacts_delete_addressbook(ctx: Context, *, name: str):
@@ -52,7 +67,10 @@ def configure_contacts_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.contacts.delete_addressbook(name=name)
@mcp.tool()
@mcp.tool(
title="Create Contact",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("contacts:write")
@instrument_tool
async def nc_contacts_create_contact(
@@ -70,7 +88,12 @@ def configure_contacts_tools(mcp: FastMCP):
addressbook=addressbook, uid=uid, contact_data=contact_data
)
@mcp.tool()
@mcp.tool(
title="Delete Contact",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("contacts:write")
@instrument_tool
async def nc_contacts_delete_contact(ctx: Context, *, addressbook: str, uid: str):
@@ -78,7 +101,10 @@ def configure_contacts_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.contacts.delete_contact(addressbook=addressbook, uid=uid)
@mcp.tool()
@mcp.tool(
title="Update Contact",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("contacts:write")
@instrument_tool
async def nc_contacts_update_contact(
+55 -14
View File
@@ -3,7 +3,7 @@ import logging
from httpx import HTTPStatusError, RequestError
from mcp.server.fastmcp import Context, FastMCP
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData
from mcp.types import ErrorData, ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -71,7 +71,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Import Recipe from URL",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@instrument_tool
async def nc_cookbook_import_recipe(url: str, ctx: Context) -> ImportRecipeResponse:
@@ -129,7 +132,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="List Recipes",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@instrument_tool
async def nc_cookbook_list_recipes(ctx: Context) -> ListRecipesResponse:
@@ -155,7 +161,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get Recipe",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@instrument_tool
async def nc_cookbook_get_recipe(recipe_id: int, ctx: Context) -> Recipe:
@@ -181,7 +190,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Create Recipe",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@instrument_tool
async def nc_cookbook_create_recipe(
@@ -261,7 +273,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Update Recipe",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@instrument_tool
async def nc_cookbook_update_recipe(
@@ -351,7 +366,12 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Delete Recipe",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("cookbook:write")
@instrument_tool
async def nc_cookbook_delete_recipe(
@@ -387,7 +407,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Search Recipes",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@instrument_tool
async def nc_cookbook_search_recipes(
@@ -424,7 +447,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="List Recipe Categories",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@instrument_tool
async def nc_cookbook_list_categories(ctx: Context) -> ListCategoriesResponse:
@@ -452,7 +478,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get Recipes in Category",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@instrument_tool
async def nc_cookbook_get_recipes_in_category(
@@ -489,7 +518,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="List Recipe Keywords",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@instrument_tool
async def nc_cookbook_list_keywords(ctx: Context) -> ListKeywordsResponse:
@@ -515,7 +547,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get Recipes with Keywords",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@instrument_tool
async def nc_cookbook_get_recipes_with_keywords(
@@ -550,7 +585,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Set Cookbook Configuration",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@instrument_tool
async def nc_cookbook_set_config(
@@ -594,7 +632,10 @@ def configure_cookbook_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Reindex Recipes",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@instrument_tool
async def nc_cookbook_reindex(ctx: Context) -> ReindexResponse:
+107 -25
View File
@@ -2,6 +2,7 @@ import logging
from typing import Optional
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -117,7 +118,10 @@ def configure_deck_tools(mcp: FastMCP):
# Read Tools (converted from resources)
@mcp.tool()
@mcp.tool(
title="List Deck Boards",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_boards(ctx: Context) -> list[DeckBoard]:
@@ -126,7 +130,10 @@ def configure_deck_tools(mcp: FastMCP):
boards = await client.deck.get_boards()
return boards
@mcp.tool()
@mcp.tool(
title="Get Deck Board",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_board(ctx: Context, board_id: int) -> DeckBoard:
@@ -135,7 +142,10 @@ def configure_deck_tools(mcp: FastMCP):
board = await client.deck.get_board(board_id)
return board
@mcp.tool()
@mcp.tool(
title="List Deck Stacks",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_stacks(ctx: Context, board_id: int) -> list[DeckStack]:
@@ -144,7 +154,10 @@ def configure_deck_tools(mcp: FastMCP):
stacks = await client.deck.get_stacks(board_id)
return stacks
@mcp.tool()
@mcp.tool(
title="Get Deck Stack",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_stack(ctx: Context, board_id: int, stack_id: int) -> DeckStack:
@@ -153,7 +166,10 @@ def configure_deck_tools(mcp: FastMCP):
stack = await client.deck.get_stack(board_id, stack_id)
return stack
@mcp.tool()
@mcp.tool(
title="List Deck Cards",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_cards(
@@ -166,7 +182,10 @@ def configure_deck_tools(mcp: FastMCP):
return stack.cards
return []
@mcp.tool()
@mcp.tool(
title="Get Deck Card",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_card(
@@ -177,7 +196,10 @@ def configure_deck_tools(mcp: FastMCP):
card = await client.deck.get_card(board_id, stack_id, card_id)
return card
@mcp.tool()
@mcp.tool(
title="List Deck Labels",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_labels(ctx: Context, board_id: int) -> list[DeckLabel]:
@@ -186,7 +208,10 @@ def configure_deck_tools(mcp: FastMCP):
board = await client.deck.get_board(board_id)
return board.labels
@mcp.tool()
@mcp.tool(
title="Get Deck Label",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@instrument_tool
async def deck_get_label(ctx: Context, board_id: int, label_id: int) -> DeckLabel:
@@ -197,7 +222,10 @@ def configure_deck_tools(mcp: FastMCP):
# Create/Update/Delete Tools
@mcp.tool()
@mcp.tool(
title="Create Deck Board",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_create_board(
@@ -215,7 +243,10 @@ def configure_deck_tools(mcp: FastMCP):
# Stack Tools
@mcp.tool()
@mcp.tool(
title="Create Deck Stack",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_create_stack(
@@ -232,7 +263,10 @@ def configure_deck_tools(mcp: FastMCP):
stack = await client.deck.create_stack(board_id, title, order)
return CreateStackResponse(id=stack.id, title=stack.title, order=stack.order)
@mcp.tool()
@mcp.tool(
title="Update Deck Stack",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_update_stack(
@@ -259,7 +293,12 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Delete Deck Stack",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_delete_stack(
@@ -281,7 +320,10 @@ def configure_deck_tools(mcp: FastMCP):
)
# Card Tools
@mcp.tool()
@mcp.tool(
title="Create Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_create_card(
@@ -316,7 +358,10 @@ def configure_deck_tools(mcp: FastMCP):
stackId=card.stackId,
)
@mcp.tool()
@mcp.tool(
title="Update Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_update_card(
@@ -370,7 +415,12 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Delete Deck Card",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_delete_card(
@@ -393,7 +443,10 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Archive Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_archive_card(
@@ -416,7 +469,10 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Unarchive Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_unarchive_card(
@@ -439,7 +495,10 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Reorder/Move Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_reorder_card(
@@ -472,7 +531,10 @@ def configure_deck_tools(mcp: FastMCP):
)
# Label Tools
@mcp.tool()
@mcp.tool(
title="Create Deck Label",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_create_label(
@@ -489,7 +551,10 @@ def configure_deck_tools(mcp: FastMCP):
label = await client.deck.create_label(board_id, title, color)
return CreateLabelResponse(id=label.id, title=label.title, color=label.color)
@mcp.tool()
@mcp.tool(
title="Update Deck Label",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_update_label(
@@ -516,7 +581,12 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Delete Deck Label",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_delete_label(
@@ -538,7 +608,10 @@ def configure_deck_tools(mcp: FastMCP):
)
# Card-Label Assignment Tools
@mcp.tool()
@mcp.tool(
title="Assign Label to Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_assign_label_to_card(
@@ -562,7 +635,10 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Remove Label from Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_remove_label_from_card(
@@ -587,7 +663,10 @@ def configure_deck_tools(mcp: FastMCP):
)
# Card-User Assignment Tools
@mcp.tool()
@mcp.tool(
title="Assign User to Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_assign_user_to_card(
@@ -611,7 +690,10 @@ def configure_deck_tools(mcp: FastMCP):
board_id=board_id,
)
@mcp.tool()
@mcp.tool(
title="Unassign User from Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@instrument_tool
async def deck_unassign_user_from_card(
+33 -9
View File
@@ -5,7 +5,7 @@ import logging
from httpx import HTTPStatusError, RequestError
from mcp.server.fastmcp import Context, FastMCP
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData
from mcp.types import ErrorData, ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.client.news import NewsItemType
@@ -30,7 +30,10 @@ logger = logging.getLogger(__name__)
def configure_news_tools(mcp: FastMCP):
"""Configure News app MCP tools."""
@mcp.tool()
@mcp.tool(
title="List News Folders",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_list_folders(ctx: Context) -> ListFoldersResponse:
@@ -52,7 +55,10 @@ def configure_news_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="List News Feeds",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_list_feeds(ctx: Context) -> ListFeedsResponse:
@@ -82,7 +88,10 @@ def configure_news_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="List News Items",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_list_items(
@@ -153,7 +162,10 @@ def configure_news_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get News Item",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_get_item(item_id: int, ctx: Context) -> GetItemResponse:
@@ -188,7 +200,10 @@ def configure_news_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get Starred News Items",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_get_starred_items(
@@ -238,7 +253,10 @@ def configure_news_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get Unread News Items",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_get_unread_items(
@@ -288,7 +306,10 @@ def configure_news_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get News Feed Health",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_get_feed_health(feed_id: int, ctx: Context) -> FeedHealthResponse:
@@ -332,7 +353,10 @@ def configure_news_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get News App Status",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@instrument_tool
async def nc_news_get_status(ctx: Context) -> GetStatusResponse:
+51 -8
View File
@@ -3,7 +3,7 @@ import logging
from httpx import HTTPStatusError, RequestError
from mcp.server.fastmcp import Context, FastMCP
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData
from mcp.types import ErrorData, ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -85,7 +85,13 @@ def configure_notes_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Create Note",
annotations=ToolAnnotations(
idempotentHint=False, # Multiple calls create multiple notes
openWorldHint=True,
),
)
@require_scopes("notes:write")
@instrument_tool
async def nc_notes_create_note(
@@ -132,7 +138,13 @@ def configure_notes_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Update Note",
annotations=ToolAnnotations(
idempotentHint=False, # Requires etag which changes = not idempotent
openWorldHint=True,
),
)
@require_scopes("notes:write")
@instrument_tool
async def nc_notes_update_note(
@@ -198,7 +210,13 @@ def configure_notes_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Append to Note",
annotations=ToolAnnotations(
idempotentHint=False, # Each call adds content = not idempotent
openWorldHint=True,
),
)
@require_scopes("notes:write")
@instrument_tool
async def nc_notes_append_content(
@@ -249,7 +267,13 @@ def configure_notes_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Search Notes",
annotations=ToolAnnotations(
readOnlyHint=True, # Search doesn't modify data
openWorldHint=True,
),
)
@require_scopes("notes:read")
@instrument_tool
async def nc_notes_search_notes(query: str, ctx: Context) -> SearchNotesResponse:
@@ -296,7 +320,13 @@ def configure_notes_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get Note",
annotations=ToolAnnotations(
readOnlyHint=True, # Read operation only
openWorldHint=True,
),
)
@require_scopes("notes:read")
@instrument_tool
async def nc_notes_get_note(note_id: int, ctx: Context) -> Note:
@@ -326,7 +356,13 @@ def configure_notes_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Get Note Attachment",
annotations=ToolAnnotations(
readOnlyHint=True, # Read operation only
openWorldHint=True,
),
)
@require_scopes("notes:read")
@instrument_tool
async def nc_notes_get_attachment(
@@ -373,7 +409,14 @@ def configure_notes_tools(mcp: FastMCP):
)
)
@mcp.tool()
@mcp.tool(
title="Delete Note",
annotations=ToolAnnotations(
destructiveHint=True, # Permanently deletes data
idempotentHint=True, # Deleting deleted note = same end state
openWorldHint=True,
),
)
@require_scopes("notes:write")
@instrument_tool
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
@@ -15,6 +15,7 @@ import httpx
from mcp.server.auth.middleware.auth_context import get_access_token
from mcp.server.auth.provider import AccessToken
from mcp.server.fastmcp import Context
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field
from nextcloud_mcp_server.auth import require_scopes
@@ -684,11 +685,16 @@ def register_oauth_tools(mcp):
@mcp.tool(
name="provision_nextcloud_access",
title="Grant Server Access to Nextcloud",
description=(
"Provision offline access to Nextcloud resources. "
"This is required before using Nextcloud tools. "
"You'll need to complete an OAuth authorization in your browser."
),
annotations=ToolAnnotations(
idempotentHint=False, # Creates new OAuth session each time
openWorldHint=True,
),
)
@require_scopes("openid")
async def tool_provision_access(
@@ -699,7 +705,13 @@ def register_oauth_tools(mcp):
@mcp.tool(
name="revoke_nextcloud_access",
title="Revoke Server Access to Nextcloud",
description="Revoke offline access to Nextcloud resources.",
annotations=ToolAnnotations(
destructiveHint=True, # Removes stored access tokens
idempotentHint=True, # Revoking revoked access = same end state
openWorldHint=True,
),
)
@require_scopes("openid")
async def tool_revoke_access(
@@ -709,7 +721,12 @@ def register_oauth_tools(mcp):
@mcp.tool(
name="check_provisioning_status",
title="Check Provisioning Status",
description="Check whether Nextcloud access is provisioned.",
annotations=ToolAnnotations(
readOnlyHint=True, # Only checks status, doesn't modify
openWorldHint=True,
),
)
@require_scopes("openid")
async def tool_check_status(
@@ -719,10 +736,15 @@ def register_oauth_tools(mcp):
@mcp.tool(
name="check_logged_in",
title="Check Server Login Status",
description=(
"Check if you are logged in to Nextcloud. "
"If not logged in, this tool will prompt you to complete the login flow."
),
annotations=ToolAnnotations(
readOnlyHint=True, # Checking status doesn't modify state
openWorldHint=True,
),
)
@require_scopes("openid")
async def tool_check_logged_in(ctx: Context, user_id: Optional[str] = None) -> str:
+22 -3
View File
@@ -12,6 +12,7 @@ from mcp.types import (
ModelPreferences,
SamplingMessage,
TextContent,
ToolAnnotations,
)
from nextcloud_mcp_server.auth import require_scopes
@@ -34,7 +35,13 @@ logger = logging.getLogger(__name__)
def configure_semantic_tools(mcp: FastMCP):
"""Configure semantic search tools for MCP server."""
@mcp.tool()
@mcp.tool(
title="Semantic Search",
annotations=ToolAnnotations(
readOnlyHint=True, # Search doesn't modify data
openWorldHint=False, # Searches only indexed Nextcloud data
),
)
@require_scopes("semantic:read")
@instrument_tool
async def nc_semantic_search(
@@ -285,7 +292,13 @@ def configure_semantic_tools(mcp: FastMCP):
logger.error(f"Search error: {e}", exc_info=True)
raise McpError(ErrorData(code=-1, message=f"Search failed: {str(e)}"))
@mcp.tool()
@mcp.tool(
title="Search with AI-Generated Answer",
annotations=ToolAnnotations(
readOnlyHint=True, # Search doesn't modify data
openWorldHint=False, # Searches only indexed Nextcloud data
),
)
@require_scopes("semantic:read")
@instrument_tool
async def nc_semantic_search_answer(
@@ -623,7 +636,13 @@ def configure_semantic_tools(mcp: FastMCP):
success=True,
)
@mcp.tool()
@mcp.tool(
title="Check Indexing Status",
annotations=ToolAnnotations(
readOnlyHint=True, # Only checks status
openWorldHint=True,
),
)
@require_scopes("semantic:read")
@instrument_tool
async def nc_get_vector_sync_status(ctx: Context) -> VectorSyncStatusResponse:
+23 -5
View File
@@ -3,6 +3,7 @@
import json
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -16,7 +17,10 @@ def configure_sharing_tools(mcp: FastMCP):
mcp: FastMCP server instance
"""
@mcp.tool()
@mcp.tool(
title="Create Share",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("sharing:write")
@instrument_tool
async def nc_share_create(
@@ -56,7 +60,12 @@ def configure_sharing_tools(mcp: FastMCP):
)
return json.dumps(share_data, indent=2)
@mcp.tool()
@mcp.tool(
title="Delete Share",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("sharing:write")
@instrument_tool
async def nc_share_delete(share_id: int, ctx: Context) -> str:
@@ -76,7 +85,10 @@ def configure_sharing_tools(mcp: FastMCP):
{"success": True, "message": f"Share {share_id} deleted"}, indent=2
)
@mcp.tool()
@mcp.tool(
title="Get Share Details",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("sharing:write")
@instrument_tool
async def nc_share_get(share_id: int, ctx: Context) -> str:
@@ -95,7 +107,10 @@ def configure_sharing_tools(mcp: FastMCP):
share_data = await client.sharing.get_share(share_id)
return json.dumps(share_data, indent=2)
@mcp.tool()
@mcp.tool(
title="List Shares",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("sharing:write")
@instrument_tool
async def nc_share_list(
@@ -117,7 +132,10 @@ def configure_sharing_tools(mcp: FastMCP):
)
return json.dumps(shares, indent=2)
@mcp.tool()
@mcp.tool(
title="Update Share",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("sharing:write")
@instrument_tool
async def nc_share_update(share_id: int, permissions: int, ctx: Context) -> str:
+27 -6
View File
@@ -1,6 +1,7 @@
import logging
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -11,7 +12,10 @@ logger = logging.getLogger(__name__)
def configure_tables_tools(mcp: FastMCP):
# Tables tools
@mcp.tool()
@mcp.tool(
title="List Tables",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("tables:read")
@instrument_tool
async def nc_tables_list_tables(ctx: Context):
@@ -19,7 +23,10 @@ def configure_tables_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.tables.list_tables()
@mcp.tool()
@mcp.tool(
title="Get Table Schema",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("tables:read")
@instrument_tool
async def nc_tables_get_schema(table_id: int, ctx: Context):
@@ -27,7 +34,10 @@ def configure_tables_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.tables.get_table_schema(table_id)
@mcp.tool()
@mcp.tool(
title="Read Table Rows",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("tables:read")
@instrument_tool
async def nc_tables_read_table(
@@ -40,7 +50,10 @@ def configure_tables_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.tables.get_table_rows(table_id, limit, offset)
@mcp.tool()
@mcp.tool(
title="Insert Table Row",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("tables:write")
@instrument_tool
async def nc_tables_insert_row(table_id: int, data: dict, ctx: Context):
@@ -51,7 +64,10 @@ def configure_tables_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.tables.create_row(table_id, data)
@mcp.tool()
@mcp.tool(
title="Update Table Row",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("tables:write")
@instrument_tool
async def nc_tables_update_row(row_id: int, data: dict, ctx: Context):
@@ -62,7 +78,12 @@ def configure_tables_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.tables.update_row(row_id, data)
@mcp.tool()
@mcp.tool(
title="Delete Table Row",
annotations=ToolAnnotations(
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("tables:write")
@instrument_tool
async def nc_tables_delete_row(row_id: int, ctx: Context):
+79 -11
View File
@@ -1,6 +1,7 @@
import logging
from mcp.server.fastmcp import Context, FastMCP
from mcp.types import ToolAnnotations
from nextcloud_mcp_server.auth import require_scopes
from nextcloud_mcp_server.context import get_client
@@ -16,7 +17,13 @@ logger = logging.getLogger(__name__)
def configure_webdav_tools(mcp: FastMCP):
# WebDAV file system tools
@mcp.tool()
@mcp.tool(
title="List Files and Directories",
annotations=ToolAnnotations(
readOnlyHint=True,
openWorldHint=True,
),
)
@require_scopes("files:read")
@instrument_tool
async def nc_webdav_list_directory(
@@ -50,7 +57,13 @@ def configure_webdav_tools(mcp: FastMCP):
total_size=total_size,
)
@mcp.tool()
@mcp.tool(
title="Read File",
annotations=ToolAnnotations(
readOnlyHint=True,
openWorldHint=True,
),
)
@require_scopes("files:read")
@instrument_tool
async def nc_webdav_read_file(path: str, ctx: Context):
@@ -117,7 +130,13 @@ def configure_webdav_tools(mcp: FastMCP):
"encoding": "base64",
}
@mcp.tool()
@mcp.tool(
title="Write File",
annotations=ToolAnnotations(
idempotentHint=False, # No etag/version control = not truly idempotent
openWorldHint=True,
),
)
@require_scopes("files:write")
@instrument_tool
async def nc_webdav_write_file(
@@ -146,7 +165,13 @@ def configure_webdav_tools(mcp: FastMCP):
return await client.webdav.write_file(path, content_bytes, content_type)
@mcp.tool()
@mcp.tool(
title="Create Directory",
annotations=ToolAnnotations(
idempotentHint=True, # Creating existing dir returns 405 = same end state
openWorldHint=True,
),
)
@require_scopes("files:write")
@instrument_tool
async def nc_webdav_create_directory(path: str, ctx: Context):
@@ -161,7 +186,14 @@ def configure_webdav_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.webdav.create_directory(path)
@mcp.tool()
@mcp.tool(
title="Delete File or Directory",
annotations=ToolAnnotations(
destructiveHint=True, # Permanently deletes data
idempotentHint=True, # Deleting deleted resource = same end state
openWorldHint=True,
),
)
@require_scopes("files:write")
@instrument_tool
async def nc_webdav_delete_resource(path: str, ctx: Context):
@@ -176,7 +208,13 @@ def configure_webdav_tools(mcp: FastMCP):
client = await get_client(ctx)
return await client.webdav.delete_resource(path)
@mcp.tool()
@mcp.tool(
title="Move or Rename File",
annotations=ToolAnnotations(
idempotentHint=False, # Moving changes source and dest
openWorldHint=True,
),
)
@require_scopes("files:write")
@instrument_tool
async def nc_webdav_move_resource(
@@ -197,7 +235,13 @@ def configure_webdav_tools(mcp: FastMCP):
source_path, destination_path, overwrite
)
@mcp.tool()
@mcp.tool(
title="Copy File or Directory",
annotations=ToolAnnotations(
idempotentHint=False, # Creates new resource each time
openWorldHint=True,
),
)
@require_scopes("files:write")
@instrument_tool
async def nc_webdav_copy_resource(
@@ -218,7 +262,13 @@ def configure_webdav_tools(mcp: FastMCP):
source_path, destination_path, overwrite
)
@mcp.tool()
@mcp.tool(
title="Search Files",
annotations=ToolAnnotations(
readOnlyHint=True,
openWorldHint=True,
),
)
@require_scopes("files:read")
@instrument_tool
async def nc_webdav_search_files(
@@ -335,7 +385,13 @@ def configure_webdav_tools(mcp: FastMCP):
filters_applied=filters if filters else None,
)
@mcp.tool()
@mcp.tool(
title="Find Files by Name",
annotations=ToolAnnotations(
readOnlyHint=True,
openWorldHint=True,
),
)
@require_scopes("files:read")
@instrument_tool
async def nc_webdav_find_by_name(
@@ -363,7 +419,13 @@ def configure_webdav_tools(mcp: FastMCP):
filters_applied={"name_pattern": pattern},
)
@mcp.tool()
@mcp.tool(
title="Find Files by Type",
annotations=ToolAnnotations(
readOnlyHint=True,
openWorldHint=True,
),
)
@require_scopes("files:read")
@instrument_tool
async def nc_webdav_find_by_type(
@@ -391,7 +453,13 @@ def configure_webdav_tools(mcp: FastMCP):
filters_applied={"mime_type": mime_type},
)
@mcp.tool()
@mcp.tool(
title="List Favorite Files",
annotations=ToolAnnotations(
readOnlyHint=True,
openWorldHint=True,
),
)
@require_scopes("files:read")
@instrument_tool
async def nc_webdav_list_favorites(