Search Events
search_eventsSearch events by keyword in title or description within a specified date range.
Instructions
Search events by keyword in title or description within a date range.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search keyword | |
| startDate | Yes | Start of range (ISO 8601, e.g. '2026-03-01T00:00:00Z') | |
| endDate | Yes | End of range (ISO 8601, e.g. '2026-03-31T23:59:59Z') | |
| limit | No | Max results (default: 50) |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| total | Yes | ||
| events | Yes |
Implementation Reference
- src/calendar/tools.ts:342-387 (handler)The tool handler function for 'search_events'. Registered via server.registerTool with input schema (query, startDate, endDate, limit) and output schema (total, events). Runs automation via Swift (search-events command) or JXA fallback (searchEventsScript).
server.registerTool( "search_events", { title: "Search Events", description: "Search events by keyword in title or description within a date range.", inputSchema: { query: z.string().max(500).describe("Search keyword"), startDate: z.string().max(64).describe("Start of range (ISO 8601, e.g. '2026-03-01T00:00:00Z')"), endDate: z.string().max(64).describe("End of range (ISO 8601, e.g. '2026-03-31T23:59:59Z')"), limit: z.number().int().min(1).max(500).optional().default(50).describe("Max results (default: 50)"), }, outputSchema: { total: z.number(), events: z.array( z.object({ id: z.string(), summary: z.string(), startDate: z.string(), endDate: z.string(), allDay: z.boolean(), calendar: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ query, startDate, endDate, limit }) => { try { const result = await runAutomation<SearchResult>({ swift: { command: "search-events", input: { query, startDate, endDate, limit }, }, jxa: () => searchEventsScript(query, startDate, endDate, limit), }); return okUntrustedStructured(result); } catch (e) { return errJxaFor("search events", e); } }, ); - src/calendar/tools.ts:347-365 (schema)Input/output schema definitions for search_events. Input: query (string), startDate (string), endDate (string), limit (number, optional, default 50). Output: total (number) and events array with id, summary, startDate, endDate, allDay, calendar.
inputSchema: { query: z.string().max(500).describe("Search keyword"), startDate: z.string().max(64).describe("Start of range (ISO 8601, e.g. '2026-03-01T00:00:00Z')"), endDate: z.string().max(64).describe("End of range (ISO 8601, e.g. '2026-03-31T23:59:59Z')"), limit: z.number().int().min(1).max(500).optional().default(50).describe("Max results (default: 50)"), }, outputSchema: { total: z.number(), events: z.array( z.object({ id: z.string(), summary: z.string(), startDate: z.string(), endDate: z.string(), allDay: z.boolean(), calendar: z.string(), }), ), }, - src/calendar/tools.ts:92-530 (registration)The registration function 'registerCalendarTools' that registers all calendar tools (including search_events) on the MCP server.
export function registerCalendarTools(server: McpServer, _config: AirMcpConfig): void { server.registerTool( "list_calendars", { title: "List Calendars", description: "List all calendars with name, color, and writable status.", inputSchema: {}, outputSchema: { calendars: z.array( z.object({ id: z.string(), name: z.string(), color: z.string().nullable(), writable: z.boolean(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async () => { try { const result = await runAutomation<CalendarItem[]>({ swift: { command: "list-calendars" }, jxa: () => listCalendarsScript(), }); return okStructured({ calendars: result }); } catch (e) { return errJxaFor("list calendars", e); } }, ); server.registerTool( "list_events", { title: "List Events", description: "List events within a date range. Requires startDate and endDate (ISO 8601). Optionally filter by calendar name. Supports limit/offset pagination.", inputSchema: { startDate: z.string().max(64).describe("Start of range (ISO 8601, e.g. '2026-03-01T00:00:00Z')"), endDate: z.string().max(64).describe("End of range (ISO 8601, e.g. '2026-03-31T23:59:59Z')"), calendar: z.string().max(500).optional().describe("Filter by calendar name"), limit: z .number() .int() .min(1) .max(1000) .optional() .default(100) .describe("Max events to return (default: 100)"), offset: z.number().int().min(0).optional().default(0).describe("Number of events to skip (default: 0)"), }, outputSchema: { total: z.number(), offset: z.number(), returned: z.number(), events: z.array( z.object({ id: z.string(), summary: z.string(), startDate: z.string(), endDate: z.string(), allDay: z.boolean(), calendar: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ startDate, endDate, calendar, limit, offset }) => { try { const result = await runAutomation<EventListResult>({ swift: { command: "list-events", input: { startDate, endDate, calendar, limit, offset }, }, jxa: () => listEventsScript(startDate, endDate, limit, offset, calendar), }); return okUntrustedStructured(result); } catch (e) { return errJxaFor("list events", e); } }, ); server.registerTool( "read_event", { title: "Read Event", description: "Read full details of a calendar event by ID. Includes attendees (read-only), location, description, and recurrence info.", inputSchema: { id: z.string().max(500).describe("Event UID"), }, outputSchema: { id: z.string(), summary: z.string(), description: z.string().nullable(), location: z.string().nullable(), startDate: z.string(), endDate: z.string(), allDay: z.boolean(), recurrence: z.string().nullable(), url: z.string().nullable(), calendar: z.string(), attendees: z.array( z.object({ name: z.string().nullable(), email: z.string().nullable(), status: z.string().nullable(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ id }) => { try { const result = await runAutomation<EventDetail>({ swift: { command: "read-event", input: { id } }, jxa: () => readEventScript(id), }); return okUntrustedStructured(result); } catch (e) { return errJxaFor("read event", e); } }, ); server.registerTool( "create_event", { title: "Create Event", description: "Create a new calendar event. Recurring events cannot be created via automation. Attendees cannot be added programmatically.", inputSchema: { summary: z.string().min(1).max(500).describe("Event title"), startDate: z.string().max(64).describe("Start date/time (ISO 8601, e.g. '2026-03-15T09:00:00Z')"), endDate: z.string().max(64).describe("End date/time (ISO 8601, e.g. '2026-03-15T10:00:00Z')"), location: z.string().max(5000).optional().describe("Event location"), description: z.string().max(5000).optional().describe("Event notes/description"), calendar: z.string().max(500).optional().describe("Target calendar name. Defaults to first writable calendar."), allDay: z.boolean().optional().describe("Set as all-day event"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ summary, startDate, endDate, location, description, calendar, allDay }) => { try { const result = await runAutomation<MutationResult>({ swift: { command: "create-event", input: { title: summary, startDate, endDate, location, notes: description, calendar, allDay }, }, jxa: () => createEventScript(summary, startDate, endDate, { location, description, calendar, allDay }), }); return ok(result); } catch (e) { return errJxaFor("create event", e); } }, ); server.registerTool( "update_event", { title: "Update Event", description: "Update event properties. Only specified fields are changed. Attendees and recurrence rules cannot be modified via automation.", inputSchema: { id: z.string().max(500).describe("Event UID"), summary: z.string().max(500).optional().describe("New title"), startDate: z .string() .max(64) .optional() .describe("New start date/time (ISO 8601, e.g. '2026-03-15T09:00:00Z')"), endDate: z.string().max(64).optional().describe("New end date/time (ISO 8601, e.g. '2026-03-15T10:00:00Z')"), location: z.string().max(5000).optional().describe("New location"), description: z.string().max(5000).optional().describe("New notes/description"), }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, }, }, async ({ id, summary, startDate, endDate, location, description }) => { try { const result = await runAutomation<MutationResult>({ swift: { command: "update-event", input: { id, title: summary, startDate, endDate, location, notes: description }, }, jxa: () => updateEventScript(id, { summary, startDate, endDate, location, description }), }); return ok(result); } catch (e) { return errJxaFor("update event", e); } }, ); server.registerTool( "delete_event", { title: "Delete Event", description: "Delete a calendar event by ID. This action is permanent.", inputSchema: { id: z.string().max(500).describe("Event UID"), }, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, }, }, async ({ id }) => { try { const result = await runAutomation<DeleteResult>({ swift: { command: "delete-event", input: { id } }, jxa: () => deleteEventScript(id), }); return ok(result); } catch (e) { return errJxaFor("delete event", e); } }, ); server.registerTool( "search_events", { title: "Search Events", description: "Search events by keyword in title or description within a date range.", inputSchema: { query: z.string().max(500).describe("Search keyword"), startDate: z.string().max(64).describe("Start of range (ISO 8601, e.g. '2026-03-01T00:00:00Z')"), endDate: z.string().max(64).describe("End of range (ISO 8601, e.g. '2026-03-31T23:59:59Z')"), limit: z.number().int().min(1).max(500).optional().default(50).describe("Max results (default: 50)"), }, outputSchema: { total: z.number(), events: z.array( z.object({ id: z.string(), summary: z.string(), startDate: z.string(), endDate: z.string(), allDay: z.boolean(), calendar: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ query, startDate, endDate, limit }) => { try { const result = await runAutomation<SearchResult>({ swift: { command: "search-events", input: { query, startDate, endDate, limit }, }, jxa: () => searchEventsScript(query, startDate, endDate, limit), }); return okUntrustedStructured(result); } catch (e) { return errJxaFor("search events", e); } }, ); server.registerTool( "get_upcoming_events", { title: "Get Upcoming Events", description: "Get the next N upcoming events from now (searches up to 30 days ahead). A convenience wrapper that doesn't require date range parameters.", inputSchema: { limit: z.number().int().min(1).max(500).optional().default(10).describe("Max events to return (default: 10)"), }, outputSchema: { total: z.number(), returned: z.number(), events: z.array( z.object({ id: z.string(), summary: z.string(), startDate: z.string(), endDate: z.string(), allDay: z.boolean(), calendar: z.string(), location: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ limit }) => { try { const result = await runAutomation<UpcomingEventsResult>({ swift: { command: "get-upcoming-events", input: { limit } }, jxa: () => getUpcomingEventsScript(limit), }); return okUntrustedStructured(result); } catch (e) { return errJxaFor("get upcoming events", e); } }, ); server.registerTool( "today_events", { title: "Today's Events", description: "Get all calendar events for today.", inputSchema: {}, outputSchema: { total: z.number(), events: z.array( z.object({ id: z.string(), summary: z.string(), startDate: z.string(), endDate: z.string(), allDay: z.boolean(), calendar: z.string(), location: z.string(), }), ), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async () => { try { const result = await runAutomation<TodayEventsResult>({ swift: { command: "today-events" }, jxa: () => todayEventsScript(), }); return okUntrustedLinkedStructured("today_events", result); } catch (e) { return errJxaFor("get today's events", e); } }, ); server.registerTool( "create_recurring_event", { title: "Create Recurring Event", description: "Create a recurring calendar event via EventKit. Supports daily, weekly, monthly, and yearly recurrence with configurable intervals. Requires macOS 26+ Swift bridge.", inputSchema: { summary: z.string().min(1).max(500).describe("Event title"), startDate: z.string().max(64).describe("Start date/time (ISO 8601, e.g. '2026-03-15T09:00:00Z')"), endDate: z.string().max(64).describe("End date/time (ISO 8601, e.g. '2026-03-15T10:00:00Z')"), location: z.string().max(5000).optional().describe("Event location"), description: z.string().max(5000).optional().describe("Event notes/description"), calendar: z.string().max(500).optional().describe("Target calendar name. Defaults to the default calendar."), recurrence: z .object({ frequency: z.enum(["daily", "weekly", "monthly", "yearly"]).describe("Recurrence frequency"), interval: z.number().int().min(1).describe("Repeat every N frequency units (e.g. 2 = every 2 weeks)"), endDate: z .string() .max(64) .optional() .describe("Recurrence end date (ISO 8601, e.g. '2026-12-31T23:59:59Z')"), count: z.number().int().min(1).optional().describe("Number of occurrences (alternative to endDate)"), daysOfWeek: z .array(z.number().int().min(1).max(7)) .optional() .describe("Days of week for weekly recurrence (1=Sun, 2=Mon, ..., 7=Sat)"), }) .describe("Recurrence rule"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ summary, startDate, endDate, location, description, calendar, recurrence }) => { try { const result = await runSwift<RecurringEventResult>( "create-recurring-event", JSON.stringify({ title: summary, startDate, endDate, location, notes: description, calendar, recurrence, }), ); return ok(result); } catch (e) { return errJxaFor("create recurring event", e); } }, ); } - src/calendar/scripts.ts:217-256 (helper)The searchEventsScript helper function that generates a JXA script string. Iterates through all calendars, filters events by date range, searches keyword in summary/description (case-insensitive), sorts by startDate, and limits results.
export function searchEventsScript(query: string, startDate: string, endDate: string, limit: number): string { return ` const Calendar = Application('Calendar'); const cals = Calendar.calendars(); const start = new Date('${esc(startDate)}'); const end = new Date('${esc(endDate)}'); const q = '${esc(query)}'.toLowerCase(); const all = []; for (const cal of cals) { const filtered = cal.events.whose({ _and: [{startDate: {_greaterThanEquals: start}}, {startDate: {_lessThanEquals: end}}] }); const count = filtered.length; if (count === 0) continue; const eSummaries = filtered.summary(); const eDescs = filtered.description(); const eUids = filtered.uid(); const eStarts = filtered.startDate(); const eEnds = filtered.endDate(); const eAllDay = filtered.alldayEvent(); const calName = cal.name(); const safe = Math.min(count, eUids.length, eSummaries.length, eDescs.length, eStarts.length, eEnds.length, eAllDay.length); for (let i = 0; i < safe; i++) { if (eUids[i] == null || eStarts[i] == null || eEnds[i] == null) continue; const summary = eSummaries[i] || ''; const desc = eDescs[i] || ''; if (summary.toLowerCase().includes(q) || desc.toLowerCase().includes(q)) { all.push({ id: eUids[i], summary: summary, startDate: eStarts[i].toISOString(), endDate: eEnds[i].toISOString(), allDay: eAllDay[i] ?? false, calendar: calName }); } } } all.sort((a, b) => new Date(a.startDate) - new Date(b.startDate)); const result = all.slice(0, ${limit}); JSON.stringify({total: all.length, returned: result.length, events: result}); `; }