-
released this
2026-08-11 22:09:03 -05:00 | 64 commits to main since this releaseRecorded retroactively on 2026-08-12. The iCloud Mail, tenant #2 section below
describes work that was already in this tag and went undescribed — the entry as published
covered only calendar management. It is written here, against the version that actually
carries the code, rather than under a later heading that would misdate it. This is the
drift the no-[Unreleased]rule exists to prevent, and it happened anyway because the
mail commits landed between a release and its successor without a version of their own.Calendar management, behind
CALENDAR_ADMIN. Creating, renaming and deleting whole
collections rather than the events inside them — and the delete is the first thing here
that cannot be undone by any later call. A wrongdelete_eventis repaired by creating
the event again; a wrongdelete_calendartakes every event in it, on every device, and
CalDAV has no trash.Like attendees before it, this shipped only because a probe cleared it first. iCloud was
asked on 2026-08-11 whether it permitsMKCALENDAR,PROPPATCHandDELETEfor an
app-specific password. It does — and the same probe found the trap the implementation is
now shaped around.Added
-
create_calendar,update_calendaranddelete_calendar, registered only when
CALENDAR_ADMINis set. Deliberately not nested underWRITABLE_CALENDARSthe way
the attendee tools are. That setting is about the events inside a collection and this
one is about the collection; a deployment can reasonably add events to a shared calendar
without being allowed to delete it, and creating a scratch calendar needs write access
to nothing that already exists. Parallel grants, not a ladder. -
A two-step delete.
delete_calendartakesexpected_name, matching the shape of
delete_event— andexpected_event_count, which does not exist anywhere else. Called
without a count it refuses and reports the real one. That refusal is the feature:
the model cannot cheaply look the number up, so serving it through a refusal is what
puts "this will delete 42 events" in front of a human before anything happens. The plan
had this open as "deleting a calendar should be harder to reach than deleting an event,
but that is not a design"; this is the design. -
count_events, aDepth: 1PROPFINDthat counts resources without reading them.
Bodies run to megabytes on a busy calendar and the count is all that is wanted.
Fixed
Both of these were found by writing the tests, not by running the code, and both would
have presented as something other than what they were.-
calendar-home-setwas read as the firsthrefin the multistatus — which is the
href of the request URI, so it resolved to the principal. New calendars would have
been created under the principal collection: no error, and nothing anyone could find.
_organizer_addressnever hit this only because itsmailto:filter excludes that href
by accident. -
The collection-level operations were filtered by the
CALENDARSallowlist. A
calendar created just now is by definition not in the allowlist, socreate_calendar
reported its own success as a failure — on every deployment that setsCALENDARS, which
is all of them. Renaming a calendar out of the allowlist failed the same way. These
operations now read the account rather than the readable subset, which also closes the
duplicate-name hole: two collections sharing a name are indistinguishable in every later
response, and the allowlist has no opinion on what exists.
Notes
-
DELETEreturns204whether or not it deleted anything. Observed live. Against a
URL built fromcalendar-home-setit answered 204 and left the collection in place;
against the href the calendar list reports, the same 204 removed it. So collections are
addressed by the href the server gave us — the 0.8.1 lesson one level up — and every
delete reads the collection back afterwards. The status is not evidence. -
A calendar created here is not readable until an operator adds it to
CALENDARS.
The tool says so in its success envelope, because otherwise the user is looking at a new
calendar thatlist_eventsinsists is empty.
Added — iCloud Mail, tenant #2 (M1, read-only)
The second tenant, and the first thing here that is not a calendar. Read-only by
construction: nothing this surface exposes can mark, move, delete or send anything, and the
settings that would grant those (MAIL_WRITABLE_FOLDERS,MAIL_SEND_ENABLED,
MAIL_FOLDER_ADMIN) deliberately do not exist yet — an unread setting reads as a granted
permission.-
Six read tools over IMAP:
list_folders,get_unread_count,list_messages,
search_messages,get_message,get_attachment. New packagemcp_gateway/icloudmail/,
mirroring the calendar's layout, withmessageid.pyinheritingeventid.py's reasoning
wholesale. -
TENANTpicks the surface, one process per tenant, never both. Defaults to
icloudcalendarso the calendar service needs no edit. Registering both tool sets in one
process would let mail'sALLOWED_SUBJECTShand out the calendar and vice versa, which is
the whole boundary — so it is a process-level choice, not a registration-level one. -
MAIL_FOLDERSis the read scope, starting atINBOX. A folder not named there is
invisible, not empty:list_foldersomits it and every read tool refuses the name
rather than answering zero. "There is nothing in that folder" is a sentence the model will
repeat to you as fact. -
Nothing is marked read by being read.
readonly=Trueon SELECT andBODY.PEEKon
fetch, verified explicitly against the live mailbox rather than assumed from the flags. -
Its own hostname,
icloudmail.mcp.jrendar.org, with its own Forgejo OAuth
application, client id, signing key,ALLOWED_SUBJECTSand NFS-backed OAuth store — a
sibling of the calendar's, never nested inside it. Probe 0 settled why on 2026-08-11: a
token is bound toPUBLIC_BASE_URL+MCP_PATHand a hostname has exactly one/token. -
deploy/nginx/icloudmail.conf, a near-copy of the calendar's block, kept in step by
the rule in its own header rather than by a shared file.
Notes on the mail tenant
-
The IMAP username is not the Apple ID. Probe 1 found the account's Apple ID is a
third-party address while IMAP authenticates as the@icloud.commail address. There is
deliberately no fallback: it would work on some accounts and fail here as
[AUTHENTICATIONFAILED], which reads as a wrong password and sends you after the one
value that was right. -
Verified through the tool layer against the live mailbox, over real HTTP with bearer
auth and driven by an MCP client rather than by Python calls, so registration, argument
coercion and the JSON envelopes were all exercised. Six refusals were checked explicitly,
because the failure that matters is an error arriving as an empty success — every one
returned an error envelope carrying no results. -
The log holds counts and error type names only. No folder name, subject, address or
body, including in the boot summary, which reports the mail address as a bare domain.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-