import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import fs from "node:fs"; import path from "node:path"; import { z } from "zod/v4"; import { EwsClient, loadConfig } from "../ews_client.ts"; export function registerCalendarTools(server: McpServer): void { server.registerTool( "get_calendar_events", { description: "Get calendar events within a date range. Set includeBody=true to fetch organizer, attendees, and body via GetItem. Returns {events, count}.", inputSchema: z.object({ startDate: z.string().describe("Start date in YYYY-MM-DD format"), endDate: z.string().describe("End date in YYYY-MM-DD format"), includeBody: z.boolean().default(false).describe( "If true, fetch full event details (organizer, attendees, body) via GetItem. Default: false", ), }), }, async ({ startDate, endDate, includeBody }) => { try { const client = new EwsClient(loadConfig()); const folderId = await client.getFolderId("calendar"); const items = await client.findCalendarItems( folderId, `${startDate}T00:00:00`, `${endDate}T23:59:59`, ); const events = []; for (const item of items) { const event: any = { subject: item.Subject ?? "(No subject)", start: item.Start ?? "", end: item.End ?? "", location: item.Location ?? item.EnhancedLocation?.DisplayName ?? "", isAllDay: item.IsAllDayEvent === "true" || item.IsAllDayEvent === true, isCancelled: item.IsCancelled === "true" || item.IsCancelled === true, isMeeting: true, isRecurring: item.IsRecurring === "true" || item.IsRecurring === true, organizer: "", organizerEmail: "", myResponse: item.MyResponseType ?? "", itemId: item.ItemId?.["@_Id"] ?? "", body: "", requiredAttendees: [], optionalAttendees: [], }; if (includeBody && event.itemId) { const details = client.extractEmailDetails(item); event.body = details.body; event.requiredAttendees = details.requiredAttendees ?? []; event.optionalAttendees = details.optionalAttendees ?? []; event.organizer = details.fromName; event.organizerEmail = details.from; event.location = details.location || event.location; } events.push(event); } return { content: [{ type: "text" as const, text: JSON.stringify({ events, count: events.length }), }], }; } catch (error: any) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: error.message ?? String(error) }), }], }; } }, ); server.registerTool( "download_event_attachments", { description: "Download all file attachments from a calendar event to disk. Inline (embedded) attachments are skipped — only standalone file attachments are downloaded. Returns {success, downloaded, count} or {success, downloaded, count, errors}.", inputSchema: z.object({ itemId: z.string().describe( "The Exchange ItemId of the calendar event", ), targetFolder: z.string().default("/tmp/attachments").describe( "Local directory to save files. Default: /tmp/attachments", ), }), }, async ({ itemId, targetFolder }) => { try { const client = new EwsClient(loadConfig()); const item = await client.getItem(itemId); const email = client.extractEmailDetails(item); const attachments = email.attachments ?? []; const fileAttachments = attachments.filter( (a: any) => a.attachmentId && !a.isInline, ); if (!fileAttachments.length) { return { content: [{ type: "text" as const, text: JSON.stringify({ success: true, downloaded: [], count: 0, message: "No downloadable file attachments.", }), }], }; } fs.mkdirSync(targetFolder, { recursive: true }); const downloaded = []; const errors = []; const usedNames = new Set(); for (const att of fileAttachments) { try { const result = await client.getAttachment(att.attachmentId); let filename = path.basename(result.name); if (!filename) filename = att.name || "attachment"; const baseName = filename; const dotIdx = baseName.lastIndexOf("."); const namePart = dotIdx > 0 ? baseName.slice(0, dotIdx) : baseName; const extPart = dotIdx > 0 ? baseName.slice(dotIdx) : ""; let counter = 1; let finalName = filename; while (usedNames.has(finalName.toLowerCase())) { finalName = extPart ? `${namePart}_${counter}${extPart}` : `${namePart}_${counter}`; counter++; } usedNames.add(finalName.toLowerCase()); const filepath = path.join(targetFolder, finalName); fs.writeFileSync(filepath, result.content); downloaded.push({ name: finalName, path: filepath, size: result.content.length, contentType: result.contentType, }); } catch (e: any) { errors.push({ name: att.name || "unknown", error: e.message ?? String(e), }); } } return { content: [{ type: "text" as const, text: JSON.stringify({ success: errors.length === 0, downloaded, count: downloaded.length, ...(errors.length ? { errors } : {}), }), }], }; } catch (error: any) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: error.message ?? String(error) }), }], }; } }, ); }