Skip to content

v5.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 09 Oct 21:19
· 19 commits to main since this release
3a2c079

Breaking changes

  • Node.js 22 or newer is required. Node.js 18 and 20 are end-of-life;
    engines, the MCP Bundle's runtime range and the CI matrix now start at 22,
    and the Node.js 18 workarounds (WebCrypto shim, Digest startup error) are
    removed (#85).
  • Write tools refuse parameters they do not take (#126). Unknown keys
    were dropped silently, so a call with a misspelled or newer parameter
    reported success for a change it never made (dav-mcp 4.3.1 drops
    cancel_occurrences and answers "Updated 0 field(s)"). The create, update
    and delete tools now answer such a call with a validation error naming the
    parameter.
  • update_event and update_todo refuse EXDATE and RDATE in
    fields
    (#126), with a validation error naming cancel_occurrences /
    restore_occurrences. Written as a field, either list replaced every value
    already in the series. Single occurrences are cancelled and restored with
    cancel_occurrences / restore_occurrences (see Fixed); extra dates
    (RDATE) of a series are edited only through update_event_raw /
    update_todo_raw.
  • Moving a recurring event or todo moves the whole series (#107). A new
    start given to update_event (or DTSTART to update_todo) now takes every
    occurrence along: moved and cancelled occurrences, extra dates and the end
    of the series shift by the same amount, so they keep applying to the
    occurrences they were made for. A weekday or day of month the rule only
    repeats from the start follows it: a series every Monday, moved to a Tuesday,
    becomes every Tuesday. Where the rule cannot follow (every Monday and
    Wednesday, moved by one day), the update is refused and says what to give
    instead in the same call. Likewise, a new rule that would leave a moved or
    cancelled occurrence on a date the series no longer has is refused. The
    reply says what else moved: the old and new rule and how many changed,
    cancelled and extra dates went along.
    This includes starting a series later: a start several weeks later used to
    leave cancelled and moved occurrences where they were; it now takes them
    along, so a cancellation can land on a meeting nobody cancelled. To start a
    series later without moving it, give the new start together with an
    RRULE that keeps the occurrences, and fix its exceptions with
    restore_occurrences / cancel_occurrences in the same call; a series
    with changed occurrences (overrides) to keep in place is rewritten with the
    raw tools. The start_date and DTSTART descriptions now say it is the
    series' first start, not the occurrence a listing showed.
  • Times without a zone are read in the calendar's time zone (#117). An
    event written without a zone (DTSTART:20261010T090000) or as an all-day
    date was placed on the clock of the machine dav-mcp runs on, so an HTTP or
    Docker deployment in UTC put a 09:00 Berlin meeting at 09:00 UTC: wrong in
    freebusy_query, in time-range searches and in listings. Such times are now
    read in the calendar's own time zone, as CalDAV specifies, and only for a
    calendar without one in the zone dav-mcp runs in (TZ). All-day events
    now cover the calendar's day rather than the UTC day. This applies to every
    tool that reads events or todos, and to the times without a zone given to
    the create and update tools: create_event with 2026-10-26T09:00:00 in a
    Berlin calendar starts at 09:00 Berlin time on any server. Since servers
    apply their own reading to a time-range search (Nextcloud and Baïkal read
    such times as UTC), list_events, calendar_query and freebusy_query
    ask the server for a wider range and decide themselves what falls inside: a
    recurring event with no occurrence in the range is no longer listed with a
    note saying so, it is left out.
  • make_calendar and update_calendar refuse a time zone that is no IANA
    name
    (#78). make_calendar used to accept any timezone and not apply
    it; a name such as CEST, Berlin or +02:00 is now invalid input, and
    nothing is sent. Any IANA zone name, UTC included, is taken (see
    Changed).
  • freebusy_query prints local times, no longer UTC (#125). Free and
    busy slots and the window were printed in UTC (01:30 PM UTC) while
    list_events and calendar_query print local times (03:30 PM GMT+2), so
    an answer to "when am I free?" could pass UTC times off as local ones. They
    are now printed in the calendar's time zone, the same wall times the
    listings show, and a new Time zone line names it. Several calendars that
    set the same zone are shown in it; calendars in different zones are shown
    in the zone dav-mcp runs in, and the line says which calendar has which.
  • A failed login is a tool error with its own code (#123). Over stdio,
    a login that failed at startup is retried on the first tool call; its
    failure used to escape the call as a JSON-RPC internal error (-32603). It
    is now the tool's error result: an authentication error (-32003) when the
    server or the OAuth token endpoint refused the credentials, a network
    error (-32004) when the server could not be reached, and a new
    configuration error (-32008) when it answered but not as a DAV server (a
    405, 404 or other 4xx to the login, or a web page), saying to check the
    server URL; a wrong AUTH_METHOD or missing setting has that code too.
    OAuth without GOOGLE_USER now stops the server at startup, like every
    other missing setting, instead of failing each tool call.
  • Lookup and input mistakes carry their own error code; nothing is
    guessed from a message any more
    (#115). Every tool that takes a
    calendar_url or addressbook_url looks it up first, and one that names
    none of the collections is a not-found error (-32006); it used to be
    classified by the words in the message, which carries the URL, so on a host
    like caldav.icloud.com it came back as a CalDAV server error (-32000,
    -32001 for CardDAV), and addressbook_query searched nothing. An
    event_url, todo_url or vcard_url with nothing behind it is a not-found
    error too. Data the caller wrote (the raw update tools, field updates,
    creates) that the server refuses as invalid (400, 415, 422) is a validation
    error (-32002); the same status on a request dav-mcp builds itself stays an
    internal error. An error without a type or status is now always an
    internal error (-32603). Errors tsdav throws for an error status
    (DAVResponseError, DAVAuthenticationError, @philflow/tsdav 2.5.0) are
    classified by that status: a refused login during a call (an OAuth refresh
    token that expired, a 401 on a calendar query) is an authentication error
    (-32003) with the settings hint, a 404 not found, a 429 a network error. A
    server that cannot be reached (fetch failed with ECONNREFUSED,
    ENOTFOUND, a timeout, ...) is a network error (-32004) that says to check
    the server URL; it used to be invalid params (-32602). A write refused
    because the calendar's time zone could not be looked up is reported by
    what the lookup ran into (a 401 as a refused login, no answer as a
    network error); a zone value the server stores but nobody can read stays
    a CalDAV error.
  • Invalid input is a validation error (-32002), no longer an internal
    error (-32603)
    (#107, refs #115 in part). A refused series change (above)
    keeps the reason and the suggested rule, and names the tools to use
    instead. An event ending before it starts, or a todo moved by DTSTART
    alone past its due date, is a validation error too, and the todo case says
    to give DTSTART and DUE together. A date or rule the update tools
    cannot read (EXDATE: "garbage", an unknown rule part) is a validation
    error that names the property to correct, for events, to-dos and contacts
    alike. A stored object that cannot be parsed is reported as a
    CalDAV/CardDAV error, with the raw tool that can replace it.
  • Weak and malformed ETags are neither accepted nor handed out (#124,
    #136). An etag parameter that cannot be an entity-tag (inner whitespace, a
    stray quote, more than 1024 characters) or is weak (W/"…") is a
    validation error before any request: If-Match compares strongly, so a weak
    one could only end in a 412. The list, query and multi-get tools passed a
    weak W/"…" getetag on as etag, and todos labelled it "required for
    updates": a model fetched, was refused, fetched the same ETag again. An
    etag field, in listings and in write results, is now only ever one the
    etag parameters take (one rule for both); a weak, missing or malformed one
    is an etag_note saying the object cannot be updated or deleted through
    dav-mcp and that the server has to be fixed (after a write: fetch the
    object first). The delete tools say what an etag_note means. CalDAV and
    CardDAV require strong ETags, and a compressing proxy, which only changes
    ETag headers, cannot cause a weak getetag. Strong ETags are shown as
    before; a todo without one no longer reads undefined.

Added

  • Listings name occurrences the way the update tools take them (#126).
    For a recurring event or todo, calendar_query, list_events,
    calendar_multi_get and the todo listings show an Occurrence ID (the
    original start of the occurrence shown, or of the first one), the
    Cancelled occurrences (EXDATE) and the Changed occurrences
    (overrides, by original start, with where each is now), all in the series'
    own form: the wall-clock time in its zone, UTC with Z, or a date. An
    exclusion stored in another zone is shown in that form too, and a date that
    excludes a whole day of a timed series is marked "(whole day)" (the marked
    text is accepted as it is). Every name is converted from the instant its
    value names — an extra date (RDATE) stored in UTC next to a Berlin series is
    named on the Berlin wall clock. A stored exclusion that names no occurrence
    (a floating EXDATE next to a zoned series, a time the rule never yields)
    cancels nothing and is listed apart, as "Exclusions that match no
    occurrence". Recurring todos now show their rule.
  • The reply of update_event/update_todo says which occurrences were
    cancelled or restored and which changed versions were removed, compared by
    occurrence, not by the stored text.
  • Listed events show their STATUS (e.g. CANCELLED; for a series, the
    occurrence listed), and freebusy_query's event details list exactly the
    occurrences that make up the busy time.

Changed

  • Docker builds and CI no longer pull from Docker Hub (#140), which
    refused CI runners' anonymous pulls with 429. The image's build stage pulls
    Node.js from ECR Public, the Docker Official Images mirror, at the same
    digest, and the Dockerfile uses BuildKit's built-in frontend instead of
    fetching docker/dockerfile. The CI image builds take QEMU and BuildKit
    from ECR Public at Docker Hub's digests. The smoke tests' CalDAV backend
    is Radicale 3.8.3 from PyPI, pinned with hashes, on the official Python
    image from ECR Public, configured like the tomsquest/docker-radicale:latest
    image it replaces.
  • Dependencies: @philflow/tsdav 2.5.0 and @philflow/tsdav-utils 0.7.0.
    tsdav for its typed errors (DAVResponseError, DAVAuthenticationError)
    that carry the status and URL of a failed request; dav-mcp classifies
    errors by them (#115, #123). tsdav-utils for its bounded, zone-correct
    occurrence expansion and its occurrence edits (#98, #107, #126).
  • Calendars get a real time zone (#78). make_calendar now applies
    timezone (it used to create the calendar without one and say so), and
    update_calendar sends it as the VTIMEZONE CalDAV asks for instead of the
    bare name (Europe/Berlin), which stricter servers reject or ignore.
    update_calendar reports the zone the server holds afterwards, and
    list_calendars shows each calendar's zone.
  • One more request for a calendar's time zone (#117). Tools that know a
    calendar only by its URL ask the server for its time zone once per call:
    calendar_multi_get, list_todos, todo_multi_get (once per task list),
    create_todo with a due date, and update_event / update_todo when they
    write a date. Time-range searches (list_events, calendar_query,
    freebusy_query) ask the server for 26 hours more on each side and filter
    the extra events out themselves, which costs some transfer on busy
    calendars.
  • A calendar time zone that cannot be read is reported (#117). A read
    goes ahead in the zone dav-mcp runs in and says so in a note; a write that
    would need the zone (a time without a zone, a move of an event stored
    without one) is refused, so nothing lands an hour off unsaid. A local time
    the clock change skips or shows twice is refused as well where it would
    otherwise be stored shifted or shortened, with what to give instead; for
    an event with its own time zone too, where a time without an offset in
    the repeated autumn hour was silently taken as the first.
  • A time given in UTC keeps an event in its own time zone (#107). A
    start like 2026-10-06T08:00:00Z for a weekly 09:00 Europe/Berlin series
    used to turn the whole series into UTC, so after the change to winter time
    every meeting sat an hour earlier in Berlin. The time is now written in the
    event's (or to-do's) own zone at the same instant: 10:00 in Berlin, every
    week.
  • Moving a series without a time zone keeps it without one (#128). An
    event stored with a "floating" time (DTSTART:20270402T113000) was written
    back in UTC when moved with update_event, so it stopped following the
    calendar's local time and its changed occurrences sat off by the server's
    UTC offset. It now stays floating: a new start without a zone is kept as
    given, and one with Z or an offset becomes its local time in the
    calendar's time zone (2027-04-03T09:30:00Z in a Berlin calendar is 11:30).
    The same goes for update_todo on a todo without a zone.
  • Single occurrences are moved or retitled with the raw tools (#107).
    update_event and update_todo edit the whole series and refuse
    RECURRENCE-ID, which would have turned the series into one occurrence. To
    move or retitle one occurrence, fetch the object with calendar_multi_get /
    todo_multi_get and send it back with update_event_raw /
    update_todo_raw; to cancel one, use cancel_occurrences (#126).
  • An update that asks for nothing writes nothing. update_event and
    update_todo without fields, dates or occurrence names (empty lists count
    as none), or whose occurrences are already as asked, reply "not changed"
    instead of writing the object back with a new etag.
  • update_calendar finds the calendar under another spelling of its URL
    (an escaped character, a trailing slash) when it reads the result back. If
    it still is not among the calendars the server lists, it says that the
    update was accepted but cannot be confirmed, instead of an internal error.

Fixed

  • Cancelling one occurrence no longer brings back the ones cancelled
    before
    (#126). update_event wrote fields.EXDATE as the complete list of
    exclusions, so a model that wrote one date to cancel one more occurrence
    dropped every earlier exclusion and those occurrences came back.
    update_event and update_todo now take cancel_occurrences and
    restore_occurrences: each names an occurrence by its original start, as
    the listings show it, and adds to or takes from the exclusions without
    touching the others (tsdav-utils cancelOccurrences/restoreOccurrences).
    Cancelling a changed occurrence removes its changed version too; restoring
    one occurrence of a day excluded as a whole brings back only that one. A
    name in the wrong form is refused rather than cancelling another
    occurrence: a time without a zone on a UTC series is not read in the
    host's zone. A refused name comes back as a validation error naming it,
    with the occurrences that day (or the cancelled ones) and how the series
    names occurrences, and saying to use the name exactly as listed. Names
    refer to the series as it is before the call; restores, then cancels, then
    fields and dates are applied, so a move in the same call takes the new
    exclusions along. On an event or todo that does not recur both are refused
    (instead of excluding its only occurrence). A new RRULE that would leave an
    exclusion naming no occurrence is refused as a validation error that names
    the way out: restore_occurrences in the same call, or the raw tool.
  • update_todo writes into the to-do, not an event next to it (#107).
    For a calendar object holding both an event and a to-do, update_todo wrote
    its fields (a new due date, say) into the event. It now writes into the
    to-do, update_event into the event, and either tool refuses an object that
    holds nothing of its kind and names the other tool.
  • An etag passed without its quotes no longer fails the write with 412
    (#124). The update and delete tools sent the etag parameter unchanged as
    If-Match, so abc123 instead of the "abc123" the server returned never
    matched its entity-tag, and Baïkal and Nextcloud refused the write as if the
    object had changed. Every etag parameter is now normalised in one place: a
    bare tag gets its quotes, a quoted one is kept, and surrounding whitespace
    is dropped. A stale etag is still refused with 412, quoted or not; when
    dav-mcp added the quotes, the error says so, since a server that does not
    quote its ETags fails the same way.
  • One contact or event with a huge parameter list no longer stalls the
    server
    (#109). ical.js reads parameters in time that grows with their
    number times the line length: a 1 MB vCard line with 500 000 parameters
    took 11 s to read, blocking every other request meanwhile, in listings,
    filters and edits alike. dav-mcp now refuses a property with more than
    1 000 parameters before parsing, vCards and calendar objects alike (real
    ones carry fewer than ten). update_contact, update_event and
    update_todo refuse to edit it with a validation error naming the
    property; both take milliseconds now.
  • Listings say which objects they could not read, and why. An event,
    contact or todo that does not parse (over the bound above, or not valid
    iCalendar/vCard) used to show up as an empty "Untitled" entry, or not at
    all once a query filter ran. Lists now count them up front and mark each
    with the reason and the tools to replace or delete it; the query tools
    name the ones no filter could search; freebusy_query reports its answer
    as incomplete, naming the object, instead of losing its busy time
    silently. The reason never quotes the stored text.
  • A wrong collection or object URL says what to send instead (#115).
    The error names the parameter and the URL, lists up to 20 of the calendars
    or address books there are (and how many more the list tool shows), and
    names that tool; the query tools add that the parameter can be left out.
    list_todos, create_todo, calendar_multi_get, addressbook_multi_get
    and addressbook_query used to skip the lookup; delete_calendar lists
    the calendars when the server answers 404 for a URL that is not one of
    them. A missing event, to-do or contact names the tools that return its
    current URL. A refused object says so in the caller's terms ("The server
    refused the event as invalid (415 …): … Correct the iCalendar data in
    updated_ical_data"), so the caller corrects it rather than retrying.
  • A failed login says where to fix the settings (#123). The hint used to
    point only at a .env file, which users of the Claude Code plugin and
    the Claude Desktop extension do not have (and it sat in a function nothing
    called, so the message carried no hint at all). A refused or failed login,
    and a write refused with 401 later on, now name the server and the three
    places the settings live: the plugin's options (/plugin → Installed →
    dav-mcp → Configure options), the extension's settings in Claude Desktop
    (Settings → Extensions → dav-mcp: Calendars, Contacts & Tasks),
    and the CALDAV_* environment variables (MCP client config, docker -e
    or .env) for npx, Docker and checkouts. dav-mcp cannot tell which of
    them started it, so it names all three; with OAuth, which only the
    environment variables set up, it names the GOOGLE_* variables. Whether
    the credentials were refused comes from the error tsdav throws
    (DAVAuthenticationError, @philflow/tsdav 2.5.0), so a server that answers
    401 on its DAV path and 405 on its root (Baïkal) is reported as a refused
    login, not as a CalDAV error. A login answered with another error status
    says which: a 403 is forbidden (-32003), an OAuth token endpoint answering
    429 a rate limit (-32004), instead of "cannot find principalUrl".
  • Free/busy and event queries judge each occurrence as it now stands
    (#98). A recurring series was expanded from its original dates, and every
    occurrence counted with the series' own STATUS and TRANSP. So a single
    occurrence cancelled (or marked transparent) still showed as busy, one
    moved from the 20th to the 14th was missing from the 14th and still
    reported on the 20th, and an opaque occurrence of a transparent series was
    free. Each occurrence is now taken at its effective time with its own
    STATUS and TRANSP (compared case-insensitively), in freebusy_query,
    calendar_query and list_events alike. An override is a full component:
    one without STATUS of a cancelled series is not cancelled, and blocks time.
  • One definition of "in the range" (#98). Free/busy counted an
    occurrence that overlaps the window, the event lists one that starts in it
    (end included): a meeting running into the window was busy, yet listed as
    "no occurrence falls inside the queried range". All of them now use the
    CalDAV time-range test (RFC 4791 9.9): it starts before the range ends and
    ends after it starts; one without duration counts when it starts in
    [start, end). A floating time is read the same way throughout, in the
    calendar's time zone (see Breaking changes).
  • Recurring events are expanded by tsdav-utils (#98), the reader the
    write side uses, so dav-mcp and the library agree on which occurrences a
    series has. An override replaces the occurrence its RECURRENCE-ID names,
    read in the series' frame by the instant it names: an id written in UTC
    for a series in Europe/Berlin used not to match, and at a DST change a
    local time shown twice is its first pass (RFC 5545 3.3.5; ical.js was up
    to an hour off there). A floating id is read by its digits, a date-time id
    on an all-day series by its date (a Berlin-midnight id for the 13th used
    to cancel the 12th), a date id on a timed series names that day's
    midnight, and a UTC id on a floating series names nothing. An EXDATE
    excludes by instant, a date on a timed series its whole day; a floating
    EXDATE in a zoned series, or a UTC one in a floating series, names
    nothing. A series the library cannot read (a UTC UNTIL on a floating
    series, say) is reported incomplete, with the reason, rather than guessed
    at. An all-day occurrence without DTEND or DURATION lasts its day, a timed
    one no time (RFC 5545 3.6.1); a recurring all-day event without an end
    used to block nothing. Times are shown as the library converts them, so a
    time a DST change shows twice reads with the offset of its first pass.
  • THISANDFUTURE and moved overrides (#98). A RANGE=THISANDFUTURE
    override moves every later occurrence by its own move, on its own wall
    clock; one whose RECURRENCE-ID is no occurrence, or an EXDATEd one, is
    ignored like any such override. An override moved into the range from
    anywhere in the series is found from the overrides themselves.
  • Recurring events are expanded from the range, not from their start
    (#98): from the start of the period that holds the range, for SECONDLY to
    WEEKLY rules with BYDAY, BYMONTH or a BYHOUR/BYMINUTE/BYSECOND that
    expands the rule (BYHOUR on DAILY, BYMINUTE on HOURLY); from a whole
    number of months or years later for MONTHLY and YEARLY rules where every
    month or year surely has an occurrence (a BYMONTHDAY within ±28, an
    ordinal BYDAY within ±4 of a month, BYSETPOS over the weekdays every month
    has, ...); and for a COUNT rule without BY parts. Other rules — a limiting
    BYHOUR (on HOURLY), the 31st, 29 February, a fifth Monday, week 53,
    MONTHLY with BYMONTH — are walked from DTSTART, as ical.js shifts its
    INTERVAL grid where a period is empty. freebusy_query used to walk
    every series from its start.
  • No series can stall a tool call (#98). ical.js tests rule candidates
    one by one without a bound of its own: FREQ=SECONDLY;BYHOUR=9 from 1950
    took 27 s, fifty MINUTELY;BYHOUR=10 series 45 s per call, and
    FREQ=DAILY;BYMONTH=2;BYMONTHDAY=30 never returned. Each tool call now
    has one budget of rule candidates (300 000, at most 100 000 per call into
    the library) and of time (1.5 s of expansion), whichever runs out first;
    each calendar object gets a fair share of both and at least 20 ms, so one
    heavy series cannot leave the others unexpanded. The clock is checked
    before each call into the library, so expansion takes about 1.5 s plus up
    to one call per remaining object: it grows with the number of objects and
    overrides in an answer (20 objects with 4 000 overrides each in range:
    about 10 s), and under machine load a series may come back incomplete
    where it otherwise would not. A year of 100 ordinary series takes about
    1.5 s in all. A walk that needs more than 100 000 candidates is
    incomplete even for a lone series (FREQ=DAILY;BYMONTHDAY=1,15 from
    1970, which cannot start near the range). ical.js yields a BY list in the
    order it is written (BYHOUR=17,9) and the library stops at the first
    candidate past its window, so such rules are read one period further. A
    series that needs more is never reported wrong, only incomplete:
    freebusy_query, which used to answer "Nothing blocks this window" for
    it, names it and says its busy time may be missing, and an event list
    says the series could not be expanded fully.