Files
exchange-mcp/tools/calendar.ts
2026-07-09 15:46:13 +03:00

188 lines
6.0 KiB
TypeScript

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",
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",
),
}),
},
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",
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<string>();
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) }),
}],
};
}
},
);
}