Skip to content

Fix events_fetch: date-only inputs silently ignored; missing @id on events#175

Open
ole-lilienthal wants to merge 2 commits into
mattt:mainfrom
ole-lilienthal:fix/calendar-events-fetch
Open

Fix events_fetch: date-only inputs silently ignored; missing @id on events#175
ole-lilienthal wants to merge 2 commits into
mattt:mainfrom
ole-lilienthal:fix/calendar-events-fetch

Conversation

@ole-lilienthal
Copy link
Copy Markdown

Summary

Two related fixes for the Calendar service's events_fetch tool, found while investigating reports of "two events with the same name only return one".

1. Date-only start/end inputs are silently ignored

ISO8601DateFormatter.lenientDate(fromISO8601String:) checks whether the input already carries a timezone offset to decide whether to fall through to local-time parsing. The check is a regex:

([Zz]|[+-]\d{2}(:?\d{2})?)$

That [+-]\d{2} branch matches the trailing -DD of any bare yyyy-MM-dd date — "2026-06-15" looks like it ends with a -15 offset — so date-only inputs return nil instead of falling through to the "yyyy-MM-dd" DateFormatter fallback that already exists below.

In Calendar.swift's events_fetch, parsedLenientISO8601Date returning nil is silent: hasStart / hasEnd stay false and the tool falls back to its default now → now + 1 week range. So a call like

{ "start": "2026-06-02", "end": "2026-06-15" }

actually executes as start = now, end = now + 1 week. The caller observes events from the current week only and reasonably reads it as "events outside that range got filtered" — which, when two events share a name and only one falls inside the default range, looks like "events with duplicate names get deduped".

Fix: gate the timezone-suffix check on dateString.count > 10 so it can't fire on a bare yyyy-MM-dd. Date-only inputs now reach the existing "yyyy-MM-dd" fallback parser as intended. ISO datetimes with timezone offsets continue to short-circuit as before.

2. Event from EKEvent carries no @id

Ontology.Event(_ event: EKEvent) doesn't set identifier, so emitted JSON-LD lacks @id. Other types in the same package do set it: Person.init(_ contact: CNContact) uses contact.identifier; ItemList.init(_ calendar: EKCalendar) uses calendar.calendarIdentifier. events_fetch and events_create now populate it from EKEvent.eventIdentifier before encoding, matching that convention. Stable @ids let downstream JSON-LD-aware consumers reliably distinguish two events with identical name/dates rather than treating them as the same blank node.

Fix #2 was found first; fix #1 turned out to be the actual cause of the reported symptom. Both are kept because each is correct on its own.

Verification

  • Standalone repro confirms a bare yyyy-MM-dd returned nil from lenientDate before and now parses to local midnight; ISO datetimes with Z / +HH:MM / -HH:MM suffixes still parse as before.
  • Driving imcp-server over JSON-RPC with date-only start/end arguments now returns events covering the requested range instead of stopping at the default-range boundary.
  • swift format lint --strict --recursive . passes.

Files

  • App/Extensions/Foundation+Extensions.swift — gate the timezone-suffix regex on input length > 10.
  • App/Services/Calendar.swift — set Event.identifier from EKEvent.eventIdentifier in events_fetch and events_create.

Without an identifier, Event values produced from EKEvent encoded as
JSON-LD blank nodes. Two events with the same name/dates/calendar
serialised to byte-identical objects, and downstream JSON-LD-aware
consumers collapsed them into a single node. Symptom: events_fetch
appeared to silently filter out duplicate-named events.

Populate Event.identifier from EKEvent.eventIdentifier in both
events_fetch and events_create so each event carries a stable @id,
matching what Ontology already does for Person (CNContact.identifier)
and ItemList (EKCalendar.calendarIdentifier).
ISO8601DateFormatter.lenientDate(fromISO8601String:) ran a regex to
decide whether the input already carried a timezone offset and thus
should not be re-parsed in local time. The regex
#"([Zz]|[+-]\\d{2}(:?\\d{2})?)$"# matches the trailing -DD of any
bare yyyy-MM-dd date — "2026-06-15" looks like it ends with a -15
offset — so date-only inputs returned nil instead of falling through
to the "yyyy-MM-dd" DateFormatter fallback.

In events_fetch this manifested as a silent regression: when the
caller passed e.g. start="2026-06-02", end="2026-06-15", parsing
failed for both, hasStart/hasEnd stayed false, and the tool fell back
to its 'now → now + 1 week' default range. The user saw events from
the current week and assumed events past that point had been filtered
out (in this case two same-name events where only the earlier one was
in the default range).

Gate the timezone-suffix check on dateString.count > 10 so it can't
fire on a bare yyyy-MM-dd. The day component can no longer collide
with the offset branch of the regex; date-only inputs now reach the
"yyyy-MM-dd" fallback as intended.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add link to mcp-swift-sdk in README App requests location permissions on first launch

1 participant