189 lines
6.3 KiB
TypeScript
189 lines
6.3 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. 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<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) }),
|
|
}],
|
|
};
|
|
}
|
|
},
|
|
);
|
|
}
|