Focus calendar tools on primary Home calendar

This commit is contained in:
Adolfo Reyna
2026-05-26 22:05:11 -04:00
parent ad3bac91ae
commit 088d891436
3 changed files with 65 additions and 17 deletions
+8 -3
View File
@@ -11,15 +11,20 @@ and stays on the local machine.
| `notes_list` | Search note titles and plaintext previews | | `notes_list` | Search note titles and plaintext previews |
| `notes_read` | Read a note by the ID returned by `notes_list` | | `notes_read` | Read a note by the ID returned by `notes_list` |
| `notes_create` | Create a plaintext-backed note | | `notes_create` | Create a plaintext-backed note |
| `calendar_list_calendars` | List calendars and write capability | | `calendar_list_calendars` | List calendar indexes, names, and write capability |
| `calendar_list_events` | List events in an ISO-8601 time window | | `calendar_list_events` | List events in an ISO-8601 time window from the focused `Home` calendar |
| `calendar_create_event` | Create an event on a named writable calendar | | `calendar_create_event` | Create an event in the focused `Home` calendar |
| `reminders_list_lists` | List reminder lists | | `reminders_list_lists` | List reminder lists |
| `reminders_list` | List reminders | | `reminders_list` | List reminders |
| `reminders_create` | Create a reminder | | `reminders_create` | Create a reminder |
There are no destructive tools in the initial server. 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 ## Setup
Requires macOS and Node.js 20 or newer. Requires macOS and Node.js 20 or newer.
+32 -6
View File
@@ -1,10 +1,19 @@
import { dateFromInput, runJxa } from "../apple-events.js"; import { dateFromInput, runJxa } from "../apple-events.js";
export const FOCUS_CALENDAR = Object.freeze({
calendarIndex: 2,
calendar: "Home",
});
const CALENDAR_LIST_SCRIPT = String.raw` const CALENDAR_LIST_SCRIPT = String.raw`
function run() { function run() {
const app = Application("Calendar"); const app = Application("Calendar");
return JSON.stringify(app.calendars().map(function (calendar) { return JSON.stringify(app.calendars().map(function (calendar, index) {
return { name: String(calendar.name()), writable: Boolean(calendar.writable()) }; 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++) { for (let c = 0; c < calendars.length; c++) {
const calendar = calendars[c]; const calendar = calendars[c];
const calendarName = String(calendar.name()); const calendarName = String(calendar.name());
if (input.calendarIndex !== null && c !== input.calendarIndex) continue;
if (input.calendar && calendarName !== input.calendar) 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++) { for (let e = 0; e < events.length; e++) {
const event = events[e]; const event = events[e];
const eventStart = event.startDate(); const eventStart = event.startDate();
@@ -29,6 +42,7 @@ function run(argv) {
if (eventEnd < start || eventStart > end) continue; if (eventEnd < start || eventStart > end) continue;
found.push({ found.push({
id: String(event.uid()), id: String(event.uid()),
calendarIndex: c,
calendar: calendarName, calendar: calendarName,
title: String(event.summary()), title: String(event.summary()),
start: eventStart.toISOString(), start: eventStart.toISOString(),
@@ -49,9 +63,14 @@ function run(argv) {
const app = Application("Calendar"); const app = Application("Calendar");
const calendars = app.calendars(); const calendars = app.calendars();
let destination = null; let destination = null;
let destinationIndex = null;
for (let c = 0; c < calendars.length; c++) { 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]; destination = calendars[c];
destinationIndex = c;
break; break;
} }
} }
@@ -68,6 +87,7 @@ function run(argv) {
destination.events.push(event); destination.events.push(event);
return JSON.stringify({ return JSON.stringify({
id: String(event.uid()), id: String(event.uid()),
calendarIndex: destinationIndex,
calendar: String(destination.name()), calendar: String(destination.name()),
title: String(event.summary()), title: String(event.summary()),
start: event.startDate().toISOString(), start: event.startDate().toISOString(),
@@ -79,13 +99,19 @@ export function listCalendars() {
return runJxa(CALENDAR_LIST_SCRIPT); return runJxa(CALENDAR_LIST_SCRIPT);
} }
export function listEvents({ start, end, calendar, limit }) { export function listEvents({ start, end, calendar, calendarIndex, limit }) {
dateFromInput(start, "start"); dateFromInput(start, "start");
dateFromInput(end, "end"); dateFromInput(end, "end");
if (new Date(start) > new Date(end)) { if (new Date(start) > new Date(end)) {
throw new Error("start must occur before 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) { export function createEvent(input) {
+25 -8
View File
@@ -1,6 +1,11 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod"; 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 { createNote, listNotes, readNote } from "./integrations/notes.js";
import { import {
createReminder, 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() { export function createMacMiniMcpServer() {
const server = new McpServer({ const server = new McpServer({
name: "macmini-mcp", name: "macmini-mcp",
@@ -60,28 +75,30 @@ export function createMacMiniMcpServer() {
server.tool( server.tool(
"calendar_list_calendars", "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), handled(listCalendars),
); );
server.tool( server.tool(
"calendar_list_events", "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."), start: z.string().describe("Range start as an ISO-8601 datetime."),
end: z.string().describe("Range end 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), limit: z.number().int().positive().max(250).default(50),
}, },
handled(listEvents), handled((input) => listEvents(withFocusedCalendar(input))),
); );
server.tool( server.tool(
"calendar_create_event", "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), title: z.string().min(1).max(500),
start: z.string().describe("Event start as an ISO-8601 datetime."), start: z.string().describe("Event start as an ISO-8601 datetime."),
end: z.string().describe("Event end 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(), notes: z.string().optional(),
location: z.string().optional(), location: z.string().optional(),
}, },
handled(createEvent), handled((input) => createEvent(withFocusedCalendar(input))),
); );
server.tool( server.tool(