fix(calendar): preserve floating/TZID semantics across CalDAV roundtrip (#782)
The CalDAV REPORT in `_search_events_by_date` unconditionally requested server-side `<C:expand>`. Per RFC 4791 §9.6.5 the server then normalizes every expanded DTSTART/DTEND to UTC `Z`, which destroyed two pieces of information on the read path: - RFC 5545 floating local times came back as fake-UTC (a `+00:00` suffix that did not match the stored value), so a 2:30 PM floating event was indistinguishable from a 14:30 UTC event in the MCP response. - TZID-bound events lost their IANA TZID context — a "10am America/New_York" event came back as `14:00:00+00:00`, making it impossible for callers to reconstruct DST-aware recurrence semantics. Replace `<C:expand>` with client-side recurrence expansion via the `recurring-ical-events` library (promoted from transitive to direct dep), so the wire response retains its original DTSTART format. Surface the TZID parameter as new `start_tz`/`end_tz` fields on `CalendarEventSummary`. Add an optional `timezone` (IANA name) parameter to `nc_calendar_create_event` and `nc_calendar_update_event` so callers can pin a TZID for naive input; the helper attaches `ZoneInfo(...)` and emits a paired `VTIMEZONE` component. Naive input without `timezone` continues to store as RFC 5545 floating local time (with a warning logged). Offset-aware input continues to store as UTC `Z`. Drive-by: switch the update path's DTSTART/DTEND assignment from raw `datetime` to `vDDDTypes(dt)` wrappers — the previous code produced invalid iCal like `DTSTART:2026-05-14 10:00:00+00:00` for any TZ-aware update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
728a9ae252
commit
9d5ac01f24
@@ -25,8 +25,30 @@ class CalendarEventSummary(BaseModel):
|
||||
|
||||
uid: str = Field(description="Event UID")
|
||||
summary: str = Field(description="Event summary/title")
|
||||
start: str = Field(description="Event start datetime (ISO format)")
|
||||
end: Optional[str] = Field(None, description="Event end datetime (ISO format)")
|
||||
start: str = Field(
|
||||
description=(
|
||||
"Event start datetime (ISO format). No suffix = RFC 5545 floating "
|
||||
"local time; ``+00:00`` = UTC; an explicit offset (e.g. ``-04:00``) "
|
||||
"= TZID-bound at that instant. The IANA TZID name is exposed "
|
||||
"separately as ``start_tz``."
|
||||
)
|
||||
)
|
||||
end: Optional[str] = Field(
|
||||
None,
|
||||
description=(
|
||||
"Event end datetime (ISO format). Same encoding rules as ``start``."
|
||||
),
|
||||
)
|
||||
start_tz: Optional[str] = Field(
|
||||
None,
|
||||
description=(
|
||||
"IANA timezone name when DTSTART had a TZID parameter (e.g. "
|
||||
"``America/New_York``). ``None`` for floating local or UTC."
|
||||
),
|
||||
)
|
||||
end_tz: Optional[str] = Field(
|
||||
None, description="IANA timezone name when DTEND had a TZID parameter."
|
||||
)
|
||||
all_day: bool = Field(default=False, description="Whether event is all-day")
|
||||
location: Optional[str] = Field(None, description="Event location")
|
||||
description: Optional[str] = Field(None, description="Event description")
|
||||
|
||||
Reference in New Issue
Block a user