From f51b27ba190bd4b70aad0e9095c5c4067ddc9436 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Fri, 20 Feb 2026 13:49:55 +0100 Subject: [PATCH] fix: address PR #574 third review round - Guard board.labels against None in deck_get_labels and resource - Add TODO comments for calendar_display_name in single-calendar paths - Document _raw_contact_to_model scope limitation (maps only what the client returns; expanding requires changes to vCard parsing) - Log debug warning when event has no start_datetime - Verified Table model is safe with extra fields (Pydantic v2 ignores) Co-Authored-By: Claude Opus 4.6 --- nextcloud_mcp_server/server/calendar.py | 12 ++++++++++-- nextcloud_mcp_server/server/contacts.py | 8 +++++++- nextcloud_mcp_server/server/deck.py | 5 +++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/nextcloud_mcp_server/server/calendar.py b/nextcloud_mcp_server/server/calendar.py index 8c7f939..0761a72 100644 --- a/nextcloud_mcp_server/server/calendar.py +++ b/nextcloud_mcp_server/server/calendar.py @@ -29,10 +29,14 @@ def _event_dict_to_summary(event: dict) -> CalendarEventSummary: else: categories = raw_categories + start = event.get("start_datetime", "") + if not start: + logger.debug("Event %s has no start_datetime", event.get("uid", "unknown")) + return CalendarEventSummary( uid=event.get("uid", ""), summary=event.get("title", ""), - start=event.get("start_datetime", ""), + start=start, end=event.get("end_datetime"), all_day=event.get("all_day", False), location=event.get("location") or None, @@ -240,7 +244,10 @@ def configure_calendar_tools(mcp: FastMCP): limit=limit, ) - # Enrich events with calendar context for per-event mapping + # Enrich events with calendar context for per-event mapping. + # Note: calendar_display_name is not available here without an + # extra list_calendars() call; the response-level calendar_name + # already identifies the calendar for single-calendar queries. for event in events: event["calendar_name"] = calendar_name @@ -463,6 +470,7 @@ def configure_calendar_tools(mcp: FastMCP): end_datetime=end_datetime, limit=limit, ) + # calendar_display_name not available without extra API call for event in events: event["calendar_name"] = calendar_name else: diff --git a/nextcloud_mcp_server/server/contacts.py b/nextcloud_mcp_server/server/contacts.py index dc44956..ca64af7 100644 --- a/nextcloud_mcp_server/server/contacts.py +++ b/nextcloud_mcp_server/server/contacts.py @@ -19,7 +19,13 @@ logger = logging.getLogger(__name__) def _raw_contact_to_model(raw: dict) -> Contact: - """Convert a raw contact dict from the contacts client to a Contact model.""" + """Convert a raw contact dict from the contacts client to a Contact model. + + Only maps fields the client's list_contacts() currently returns: + fullname, nickname, birthday, and email. Additional Contact model fields + (phones, addresses, organization, etc.) require expanding the client's + vCard parsing in ContactsClient.list_contacts(). + """ contact_info = raw.get("contact", {}) # Convert email field (str, list, or None) to list[ContactField] diff --git a/nextcloud_mcp_server/server/deck.py b/nextcloud_mcp_server/server/deck.py index ee6f499..f48e134 100644 --- a/nextcloud_mcp_server/server/deck.py +++ b/nextcloud_mcp_server/server/deck.py @@ -107,7 +107,7 @@ def configure_deck_tools(mcp: FastMCP): ) client = await get_client(ctx) board = await client.deck.get_board(board_id) - return [label.model_dump() for label in board.labels] + return [label.model_dump() for label in (board.labels or [])] @mcp.resource("nc://Deck/boards/{board_id}/labels/{label_id}") async def deck_label_resource(board_id: int, label_id: int): @@ -209,7 +209,8 @@ def configure_deck_tools(mcp: FastMCP): """Get all labels in a Nextcloud Deck board""" client = await get_client(ctx) board = await client.deck.get_board(board_id) - return ListLabelsResponse(labels=board.labels, total=len(board.labels)) + labels = board.labels or [] + return ListLabelsResponse(labels=labels, total=len(labels)) @mcp.tool( title="Get Deck Label",