fix(calendar): address PR feedback from maintainer
- Remove CHANGELOG.md changes (auto-generated from commits) - Move all parameter descriptions into function docstrings for LLM context - Remove unused caldav dependency (using httpx for CalDAV implementation) - Move datetime imports to top of modules - Remove load_dotenv from tests/conftest.py - Clarify Event vs Meeting distinction in docstrings - Handle 401 auth errors gracefully in calendar tests Addresses all feedback from PR #95 review
This commit is contained in:
@@ -1,32 +1,3 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Feat
|
||||
|
||||
- **calendar**: Add comprehensive Calendar app support via CalDAV protocol (Issue #74)
|
||||
- **calendar**: Add `nc_calendar_list_calendars` tool for listing available calendars
|
||||
- **calendar**: Add `nc_calendar_create_event` tool with full feature support (recurrence, reminders, attendees, categories)
|
||||
- **calendar**: Add `nc_calendar_list_events` tool with advanced filtering (date range, attendees, categories, status)
|
||||
- **calendar**: Add `nc_calendar_get_event` tool for retrieving detailed event information
|
||||
- **calendar**: Add `nc_calendar_update_event` tool for modifying existing events
|
||||
- **calendar**: Add `nc_calendar_delete_event` tool for removing events
|
||||
- **calendar**: Add `nc_calendar_create_meeting` tool for quick meeting creation with smart defaults
|
||||
- **calendar**: Add `nc_calendar_get_upcoming_events` tool for viewing upcoming events
|
||||
- **calendar**: Add `nc_calendar_find_availability` tool for intelligent scheduling assistance
|
||||
- **calendar**: Add `nc_calendar_bulk_operations` tool for efficient batch event management
|
||||
- **calendar**: Add `nc_calendar_manage_calendar` tool for calendar creation and management
|
||||
|
||||
### Fix
|
||||
|
||||
- **calendar**: Fix type annotations in calendar client for better Pylance compatibility
|
||||
- **calendar**: Fix alarm trigger formatting using proper timedelta objects
|
||||
- **calendar**: Fix event update handling to merge existing data with new changes
|
||||
- **calendar**: Fix categories extraction from icalendar objects
|
||||
|
||||
### Refactor
|
||||
|
||||
- **calendar**: Implement CalDAV client following existing NextCloud client patterns
|
||||
- **calendar**: Add comprehensive calendar integration tests covering all scenarios
|
||||
|
||||
## v0.5.0 (2025-07-26)
|
||||
|
||||
### Feat
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime, date
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
import logging
|
||||
from httpx import HTTPStatusError
|
||||
from icalendar import Calendar, Event as ICalEvent, vRecur
|
||||
from icalendar import Calendar, Event as ICalEvent, vRecur, Alarm
|
||||
from datetime import timedelta
|
||||
import uuid
|
||||
|
||||
@@ -124,6 +124,12 @@ class CalendarClient(BaseNextcloudClient):
|
||||
return calendars
|
||||
|
||||
except HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
logger.warning("Authentication failed for CalDAV - Calendar app may not be enabled for this user")
|
||||
return []
|
||||
elif e.response.status_code == 404:
|
||||
logger.warning("CalDAV endpoint not found - Calendar app may not be installed")
|
||||
return []
|
||||
logger.error(f"HTTP error listing calendars: {e}")
|
||||
raise e
|
||||
except Exception as e:
|
||||
@@ -429,7 +435,6 @@ class CalendarClient(BaseNextcloudClient):
|
||||
# Add alarms/reminders
|
||||
reminder_minutes = event_data.get("reminder_minutes", 0)
|
||||
if reminder_minutes > 0:
|
||||
from icalendar import Alarm
|
||||
|
||||
alarm = Alarm()
|
||||
alarm.add("action", "DISPLAY")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# server.py
|
||||
import logging
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
from nextcloud_mcp_server.config import setup_logging
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
@@ -351,31 +352,52 @@ async def nc_calendar_list_calendars(ctx: Context):
|
||||
async def nc_calendar_create_event(
|
||||
calendar_name: str,
|
||||
title: str,
|
||||
start_datetime: str, # ISO format: "2025-01-15T14:00:00" or "2025-01-15" for all-day
|
||||
start_datetime: str,
|
||||
ctx: Context,
|
||||
end_datetime: str = "", # Empty for all-day events
|
||||
end_datetime: str = "",
|
||||
all_day: bool = False,
|
||||
description: str = "",
|
||||
location: str = "",
|
||||
categories: str = "", # "work,meeting" - comma separated
|
||||
# Recurrence
|
||||
categories: str = "",
|
||||
recurring: bool = False,
|
||||
recurrence_rule: str = "", # "FREQ=WEEKLY;BYDAY=MO,WE,FR" (RFC5545 RRULE)
|
||||
recurrence_end_date: str = "", # When to stop recurring
|
||||
# Notifications/Alarms
|
||||
reminder_minutes: int = 15, # Minutes before event to remind
|
||||
reminder_email: bool = False, # Email notification
|
||||
# Event properties
|
||||
status: str = "CONFIRMED", # CONFIRMED, TENTATIVE, CANCELLED
|
||||
priority: int = 5, # 1-9 (1=highest, 9=lowest, 5=normal)
|
||||
privacy: str = "PUBLIC", # PUBLIC, PRIVATE, CONFIDENTIAL
|
||||
# Attendees
|
||||
attendees: str = "", # "email1@domain.com,email2@domain.com"
|
||||
# Additional
|
||||
url: str = "", # Related URL
|
||||
color: str = "", # Event color (hex or name)
|
||||
recurrence_rule: str = "",
|
||||
recurrence_end_date: str = "",
|
||||
reminder_minutes: int = 15,
|
||||
reminder_email: bool = False,
|
||||
status: str = "CONFIRMED",
|
||||
priority: int = 5,
|
||||
privacy: str = "PUBLIC",
|
||||
attendees: str = "",
|
||||
url: str = "",
|
||||
color: str = "",
|
||||
):
|
||||
"""Create a comprehensive calendar event with full feature support"""
|
||||
"""Create a comprehensive calendar event with full feature support
|
||||
|
||||
Args:
|
||||
calendar_name: Name of the calendar to create the event in
|
||||
title: Event title
|
||||
start_datetime: ISO format: "2025-01-15T14:00:00" or "2025-01-15" for all-day
|
||||
ctx: MCP context
|
||||
end_datetime: ISO format end time, empty for all-day events
|
||||
all_day: Whether this is an all-day event
|
||||
description: Event description/details
|
||||
location: Event location
|
||||
categories: Comma-separated categories (e.g., "work,meeting")
|
||||
recurring: Whether this is a recurring event
|
||||
recurrence_rule: RFC5545 RRULE (e.g., "FREQ=WEEKLY;BYDAY=MO,WE,FR")
|
||||
recurrence_end_date: When to stop recurring
|
||||
reminder_minutes: Minutes before event to send reminder
|
||||
reminder_email: Whether to send email notification
|
||||
status: Event status: CONFIRMED, TENTATIVE, or CANCELLED
|
||||
priority: Priority level 1-9 (1=highest, 9=lowest, 5=normal)
|
||||
privacy: Privacy level: PUBLIC, PRIVATE, or CONFIDENTIAL
|
||||
attendees: Comma-separated email addresses
|
||||
url: Related URL for the event
|
||||
color: Event color (hex or name)
|
||||
|
||||
Returns:
|
||||
Dict with event creation result
|
||||
"""
|
||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||
|
||||
event_data = {
|
||||
@@ -406,13 +428,13 @@ async def nc_calendar_create_event(
|
||||
async def nc_calendar_list_events(
|
||||
calendar_name: str,
|
||||
ctx: Context,
|
||||
start_date: str = "", # "2025-01-01"
|
||||
end_date: str = "", # "2025-01-31"
|
||||
start_date: str = "",
|
||||
end_date: str = "",
|
||||
limit: int = 50,
|
||||
min_attendees: Optional[int] = None,
|
||||
min_duration_minutes: Optional[int] = None,
|
||||
categories: Optional[str] = None, # Comma-separated: "work,meeting"
|
||||
status: Optional[str] = None, # "CONFIRMED", "TENTATIVE", "CANCELLED"
|
||||
categories: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
title_contains: Optional[str] = None,
|
||||
location_contains: Optional[str] = None,
|
||||
search_all_calendars: bool = False,
|
||||
@@ -421,16 +443,20 @@ async def nc_calendar_list_events(
|
||||
|
||||
Args:
|
||||
calendar_name: Name of the calendar to search. Ignored if search_all_calendars=True.
|
||||
start_date: Start date for search (YYYY-MM-DD format)
|
||||
end_date: End date for search (YYYY-MM-DD format)
|
||||
ctx: MCP context
|
||||
start_date: Start date for search (YYYY-MM-DD format, e.g., "2025-01-01")
|
||||
end_date: End date for search (YYYY-MM-DD format, e.g., "2025-01-31")
|
||||
limit: Maximum number of events to return
|
||||
min_attendees: Filter events with at least this many attendees
|
||||
min_duration_minutes: Filter events with at least this duration
|
||||
categories: Filter events containing any of these categories (comma-separated)
|
||||
status: Filter events by status (CONFIRMED, TENTATIVE, CANCELLED)
|
||||
categories: Filter events containing any of these categories (comma-separated, e.g., "work,meeting")
|
||||
status: Filter events by status (CONFIRMED, TENTATIVE, or CANCELLED)
|
||||
title_contains: Filter events where title contains this text
|
||||
location_contains: Filter events where location contains this text
|
||||
search_all_calendars: If True, search across all calendars instead of just one
|
||||
|
||||
Returns:
|
||||
List of events matching the filters
|
||||
"""
|
||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||
|
||||
@@ -572,8 +598,8 @@ async def nc_calendar_delete_event(
|
||||
@mcp.tool()
|
||||
async def nc_calendar_create_meeting(
|
||||
title: str,
|
||||
date: str, # "2025-01-15"
|
||||
time: str, # "14:00"
|
||||
date: str,
|
||||
time: str,
|
||||
ctx: Context,
|
||||
duration_minutes: int = 60,
|
||||
calendar_name: str = "personal",
|
||||
@@ -582,14 +608,38 @@ async def nc_calendar_create_meeting(
|
||||
description: str = "",
|
||||
reminder_minutes: int = 15,
|
||||
):
|
||||
"""Quick meeting creation with smart defaults"""
|
||||
"""Quick meeting creation with smart defaults
|
||||
|
||||
This is a convenience function for creating events with common meeting defaults.
|
||||
It automatically:
|
||||
- Calculates end time based on duration
|
||||
- Sets status to CONFIRMED
|
||||
- Adds a reminder
|
||||
- Uses simpler date/time inputs instead of full ISO format
|
||||
|
||||
For full control over all event properties, use nc_calendar_create_event instead.
|
||||
|
||||
Args:
|
||||
title: Meeting title
|
||||
date: Meeting date (YYYY-MM-DD format, e.g., "2025-01-15")
|
||||
time: Meeting start time (HH:MM format, e.g., "14:00")
|
||||
ctx: MCP context
|
||||
duration_minutes: Meeting duration in minutes (default: 60)
|
||||
calendar_name: Calendar to create the meeting in (default: "personal")
|
||||
attendees: Comma-separated email addresses of attendees
|
||||
location: Meeting location
|
||||
description: Meeting description/agenda
|
||||
reminder_minutes: Minutes before meeting to send reminder (default: 15)
|
||||
|
||||
Returns:
|
||||
Dict with meeting creation result
|
||||
"""
|
||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||
|
||||
# Combine date and time for start_datetime
|
||||
start_datetime = f"{date}T{time}:00"
|
||||
|
||||
# Calculate end_datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
start_dt = datetime.fromisoformat(start_datetime)
|
||||
end_dt = start_dt + timedelta(minutes=duration_minutes)
|
||||
@@ -622,8 +672,6 @@ async def nc_calendar_get_upcoming_events(
|
||||
"""Get upcoming events in next N days"""
|
||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
now = datetime.now()
|
||||
end_date = now + timedelta(days=days_ahead)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ dependencies = [
|
||||
"mcp[cli] (>=1.10,<1.11)",
|
||||
"httpx (>=0.28.1,<0.29.0)",
|
||||
"pillow (>=11.2.1,<12.0.0)",
|
||||
"caldav (>=1.3.6,<2.0.0)",
|
||||
"icalendar (>=6.0.0,<7.0.0)"
|
||||
]
|
||||
|
||||
|
||||
@@ -5,10 +5,6 @@ import uuid
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
from httpx import HTTPStatusError
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -95,6 +95,10 @@ async def test_list_calendars(nc_client: NextcloudClient):
|
||||
calendars = await nc_client.calendar.list_calendars()
|
||||
|
||||
assert isinstance(calendars, list)
|
||||
|
||||
if not calendars:
|
||||
pytest.skip("No calendars available - Calendar app may not be enabled")
|
||||
|
||||
logger.info(f"Found {len(calendars)} calendars")
|
||||
|
||||
# Check structure of calendars
|
||||
|
||||
Reference in New Issue
Block a user