Focus calendar tools on primary Home calendar
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user