From 088d8914365e156f729925e878e7635257f2fc73 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Tue, 26 May 2026 22:05:11 -0400 Subject: [PATCH] Focus calendar tools on primary Home calendar --- README.md | 11 ++++++++--- src/integrations/calendar.js | 38 ++++++++++++++++++++++++++++++------ src/server.js | 33 +++++++++++++++++++++++-------- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index b34a41a..abd4f0e 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,20 @@ and stays on the local machine. | `notes_list` | Search note titles and plaintext previews | | `notes_read` | Read a note by the ID returned by `notes_list` | | `notes_create` | Create a plaintext-backed note | -| `calendar_list_calendars` | List calendars and write capability | -| `calendar_list_events` | List events in an ISO-8601 time window | -| `calendar_create_event` | Create an event on a named writable calendar | +| `calendar_list_calendars` | List calendar indexes, names, and write capability | +| `calendar_list_events` | List events in an ISO-8601 time window from the focused `Home` calendar | +| `calendar_create_event` | Create an event in the focused `Home` calendar | | `reminders_list_lists` | List reminder lists | | `reminders_list` | List reminders | | `reminders_create` | Create a reminder | There are no destructive tools in the initial server. +Calendar names can repeat across accounts. This server is focused on the +event-rich `Home` calendar discovered during setup (`calendarIndex: 2`) and +verifies the selected index is still named `Home` before operating on it. +Calendar selector parameters remain available as advanced overrides. + ## Setup Requires macOS and Node.js 20 or newer. diff --git a/src/integrations/calendar.js b/src/integrations/calendar.js index 3a668b9..2b11518 100644 --- a/src/integrations/calendar.js +++ b/src/integrations/calendar.js @@ -1,10 +1,19 @@ import { dateFromInput, runJxa } from "../apple-events.js"; +export const FOCUS_CALENDAR = Object.freeze({ + calendarIndex: 2, + calendar: "Home", +}); + const CALENDAR_LIST_SCRIPT = String.raw` function run() { const app = Application("Calendar"); - return JSON.stringify(app.calendars().map(function (calendar) { - return { name: String(calendar.name()), writable: Boolean(calendar.writable()) }; + return JSON.stringify(app.calendars().map(function (calendar, index) { + return { + index: index, + name: String(calendar.name()), + writable: Boolean(calendar.writable()) + }; })); }`; @@ -20,8 +29,12 @@ function run(argv) { for (let c = 0; c < calendars.length; c++) { const calendar = calendars[c]; const calendarName = String(calendar.name()); + if (input.calendarIndex !== null && c !== input.calendarIndex) continue; if (input.calendar && calendarName !== input.calendar) continue; - const events = calendar.events(); + const events = calendar.events.whose({ + startDate: {_lessThanEquals: end}, + endDate: {_greaterThanEquals: start} + })(); for (let e = 0; e < events.length; e++) { const event = events[e]; const eventStart = event.startDate(); @@ -29,6 +42,7 @@ function run(argv) { if (eventEnd < start || eventStart > end) continue; found.push({ id: String(event.uid()), + calendarIndex: c, calendar: calendarName, title: String(event.summary()), start: eventStart.toISOString(), @@ -49,9 +63,14 @@ function run(argv) { const app = Application("Calendar"); const calendars = app.calendars(); let destination = null; + let destinationIndex = null; for (let c = 0; c < calendars.length; c++) { - if (String(calendars[c].name()) === input.calendar) { + const indexMatches = input.calendarIndex !== null && c === input.calendarIndex && + (!input.calendar || String(calendars[c].name()) === input.calendar); + const nameMatches = input.calendarIndex === null && String(calendars[c].name()) === input.calendar; + if (indexMatches || nameMatches) { destination = calendars[c]; + destinationIndex = c; break; } } @@ -68,6 +87,7 @@ function run(argv) { destination.events.push(event); return JSON.stringify({ id: String(event.uid()), + calendarIndex: destinationIndex, calendar: String(destination.name()), title: String(event.summary()), start: event.startDate().toISOString(), @@ -79,13 +99,19 @@ export function listCalendars() { return runJxa(CALENDAR_LIST_SCRIPT); } -export function listEvents({ start, end, calendar, limit }) { +export function listEvents({ start, end, calendar, calendarIndex, limit }) { dateFromInput(start, "start"); dateFromInput(end, "end"); if (new Date(start) > new Date(end)) { throw new Error("start must occur before end."); } - return runJxa(EVENTS_LIST_SCRIPT, { start, end, calendar, limit }); + return runJxa(EVENTS_LIST_SCRIPT, { + start, + end, + calendar, + calendarIndex: calendarIndex ?? null, + limit, + }); } export function createEvent(input) { diff --git a/src/server.js b/src/server.js index 08e40ba..0fb9c43 100644 --- a/src/server.js +++ b/src/server.js @@ -1,6 +1,11 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { createEvent, listCalendars, listEvents } from "./integrations/calendar.js"; +import { + FOCUS_CALENDAR, + createEvent, + listCalendars, + listEvents, +} from "./integrations/calendar.js"; import { createNote, listNotes, readNote } from "./integrations/notes.js"; import { createReminder, @@ -23,6 +28,16 @@ const handled = (operation) => async (input) => { } }; +function withFocusedCalendar(input) { + if (input.calendarIndex !== undefined) { + return { ...input, calendarIndex: input.calendarIndex }; + } + if (input.calendar !== undefined) { + return { ...input, calendarIndex: null }; + } + return { ...input, ...FOCUS_CALENDAR }; +} + export function createMacMiniMcpServer() { const server = new McpServer({ name: "macmini-mcp", @@ -60,28 +75,30 @@ export function createMacMiniMcpServer() { server.tool( "calendar_list_calendars", - "List macOS Calendar calendars and indicate which accept new events.", + "List macOS Calendar calendars with indexes and indicate which accept new events. The server is focused on the event-rich Home calendar by default.", {}, handled(listCalendars), ); server.tool( "calendar_list_events", - "List Calendar events intersecting a required ISO-8601 time range.", + "List events in the focused Home calendar over a required ISO-8601 time range. Optional selectors override the focus calendar.", { start: z.string().describe("Range start as an ISO-8601 datetime."), end: z.string().describe("Range end as an ISO-8601 datetime."), - calendar: z.string().optional().describe("Exact calendar name."), + calendar: z.string().optional().describe("Advanced override: exact calendar name."), + calendarIndex: z.number().int().nonnegative().optional().describe("Advanced override: index returned by calendar_list_calendars."), limit: z.number().int().positive().max(250).default(50), }, - handled(listEvents), + handled((input) => listEvents(withFocusedCalendar(input))), ); server.tool( "calendar_create_event", - "Create an event on a named writable macOS calendar.", + "Create an event in the focused Home calendar. Optional selectors override the focus calendar.", { - calendar: z.string().min(1).describe("Exact destination calendar name."), + calendar: z.string().optional().describe("Advanced override: exact destination calendar name."), + calendarIndex: z.number().int().nonnegative().optional().describe("Advanced override: destination index."), title: z.string().min(1).max(500), start: z.string().describe("Event start as an ISO-8601 datetime."), end: z.string().describe("Event end as an ISO-8601 datetime."), @@ -89,7 +106,7 @@ export function createMacMiniMcpServer() { notes: z.string().optional(), location: z.string().optional(), }, - handled(createEvent), + handled((input) => createEvent(withFocusedCalendar(input))), ); server.tool(