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_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.
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user