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_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.
+32 -6
View File
@@ -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) {
+25 -8
View File
@@ -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(