From 509e49e37491f710465f1138aea98929fa122b54 Mon Sep 17 00:00:00 2001 From: albnnc Date: Thu, 9 Jul 2026 15:46:13 +0300 Subject: [PATCH] w --- ews_client.ts | 31 +++++++++---- main.ts | 4 +- tools/availability.ts | 100 +++++++++++++++++++++++++++++++----------- tools/calendar.ts | 32 ++++++++++---- tools/email.ts | 24 +++++++--- tools/people.ts | 3 +- 6 files changed, 143 insertions(+), 51 deletions(-) diff --git a/ews_client.ts b/ews_client.ts index 84415ef..8c2f983 100644 --- a/ews_client.ts +++ b/ews_client.ts @@ -537,12 +537,16 @@ ${restriction} const changesXml = itemIds.map((id) => { const updatesXml = updates.map((u) => { - const val = typeof u.value === "boolean" ? (u.value ? "true" : "false") : u.value; + const val = typeof u.value === "boolean" + ? (u.value ? "true" : "false") + : u.value; return `\ - ${String(val)} + ${String(val)} `; }).join("\n"); @@ -573,7 +577,9 @@ ${changesXml} // ── GetAttachment ─────────────────────────────────────────────────────── - async getAttachment(attachmentId: string): Promise<{ content: Buffer; name: string; contentType: string }> { + async getAttachment( + attachmentId: string, + ): Promise<{ content: Buffer; name: string; contentType: string }> { const soap = buildSoapEnvelope(`\ @@ -645,13 +651,17 @@ ${changesXml} endDate: string, requestedView: string = "DetailedMerged", ): Promise { - const mailboxDataXml = emails.map((email) => `\ + const mailboxDataXml = emails.map((email) => + `\ - ${email.replace(/&/g, "&").replace(/ + ${ + email.replace(/&/g, "&").replace(/ Required - `).join("\n"); + ` + ).join("\n"); const soap = ` { + async resolveNames( + query: string, + fullContact: boolean = true, + ): Promise { const soap = buildSoapEnvelope(`\ - ${query.replace(/&/g, "&").replace(/ + ${ + query.replace(/&/g, "&").replace(/ `); const data = await this.soapRequest( diff --git a/main.ts b/main.ts index 097250e..3ac4048 100644 --- a/main.ts +++ b/main.ts @@ -6,10 +6,10 @@ import crypto from "node:crypto"; import http from "node:http"; import { initDataDir } from "./ews_client.ts"; import { registerAuthTools } from "./tools/auth.ts"; +import { registerAvailabilityTools } from "./tools/availability.ts"; +import { registerCalendarTools } from "./tools/calendar.ts"; import { registerEmailTools } from "./tools/email.ts"; import { registerFolderTools } from "./tools/folders.ts"; -import { registerCalendarTools } from "./tools/calendar.ts"; -import { registerAvailabilityTools } from "./tools/availability.ts"; import { registerPeopleTools } from "./tools/people.ts"; const program = new Command() diff --git a/tools/availability.ts b/tools/availability.ts index 890fdc9..d412177 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -9,10 +9,18 @@ export function registerAvailabilityTools(server: McpServer): void { description: "Find free time slots in your own calendar", inputSchema: z.object({ startDate: z.string().describe("Start date in YYYY-MM-DD format"), - endDate: z.string().optional().describe("End date in YYYY-MM-DD format (defaults to startDate)"), - durationMinutes: z.number().default(30).describe("Minimum slot duration in minutes (default 30)"), - startHour: z.number().default(9).describe("Working day start hour (0-23, default 9)"), - endHour: z.number().default(18).describe("Working day end hour (0-23, default 18)"), + endDate: z.string().optional().describe( + "End date in YYYY-MM-DD format (defaults to startDate)", + ), + durationMinutes: z.number().default(30).describe( + "Minimum slot duration in minutes (default 30)", + ), + startHour: z.number().default(9).describe( + "Working day start hour (0-23, default 9)", + ), + endHour: z.number().default(18).describe( + "Working day end hour (0-23, default 18)", + ), }), }, async ({ startDate, endDate, durationMinutes, startHour, endHour }) => { @@ -27,9 +35,13 @@ export function registerAvailabilityTools(server: McpServer): void { ed, ); - const body = data?.Envelope?.Body?.GetUserAvailabilityResponse ?? data?.Envelope?.Body ?? {}; - const freeBusyArray = body?.FreeBusyResponseArray?.FreeBusyResponse ?? []; - const responses = Array.isArray(freeBusyArray) ? freeBusyArray : [freeBusyArray]; + const body = data?.Envelope?.Body?.GetUserAvailabilityResponse + ?? data?.Envelope?.Body ?? {}; + const freeBusyArray = body?.FreeBusyResponseArray?.FreeBusyResponse + ?? []; + const responses = Array.isArray(freeBusyArray) + ? freeBusyArray + : [freeBusyArray]; const busyPeriods: { start: Date; end: Date }[] = []; @@ -63,7 +75,10 @@ export function registerAvailabilityTools(server: McpServer): void { ); return { - content: [{ type: "text" as const, text: JSON.stringify({ freeSlots }) }], + content: [{ + type: "text" as const, + text: JSON.stringify({ freeSlots }), + }], }; } catch (error: any) { return { @@ -81,25 +96,43 @@ export function registerAvailabilityTools(server: McpServer): void { { description: "Find meeting times that work for multiple people", inputSchema: z.object({ - emails: z.string().describe("Comma-separated email addresses of attendees"), + emails: z.string().describe( + "Comma-separated email addresses of attendees", + ), startDate: z.string().describe("Start date in YYYY-MM-DD format"), - endDate: z.string().optional().describe("End date in YYYY-MM-DD format (defaults to startDate)"), - durationMinutes: z.number().default(30).describe("Minimum slot duration in minutes (default 30)"), - startHour: z.number().default(9).describe("Working day start hour (0-23, default 9)"), - endHour: z.number().default(18).describe("Working day end hour (0-23, default 18)"), + endDate: z.string().optional().describe( + "End date in YYYY-MM-DD format (defaults to startDate)", + ), + durationMinutes: z.number().default(30).describe( + "Minimum slot duration in minutes (default 30)", + ), + startHour: z.number().default(9).describe( + "Working day start hour (0-23, default 9)", + ), + endHour: z.number().default(18).describe( + "Working day end hour (0-23, default 18)", + ), }), }, - async ({ emails, startDate, endDate, durationMinutes, startHour, endHour }) => { + async ( + { emails, startDate, endDate, durationMinutes, startHour, endHour }, + ) => { try { const client = new EwsClient(loadConfig()); - const emailList = emails.split(",").map((e) => e.trim()).filter(Boolean); + const emailList = emails.split(",").map((e) => e.trim()).filter( + Boolean, + ); const ed = endDate || startDate; const data = await client.getUserAvailability(emailList, startDate, ed); - const body = data?.Envelope?.Body?.GetUserAvailabilityResponse ?? data?.Envelope?.Body ?? {}; - const freeBusyArray = body?.FreeBusyResponseArray?.FreeBusyResponse ?? []; - const responses = Array.isArray(freeBusyArray) ? freeBusyArray : [freeBusyArray]; + const body = data?.Envelope?.Body?.GetUserAvailabilityResponse + ?? data?.Envelope?.Body ?? {}; + const freeBusyArray = body?.FreeBusyResponseArray?.FreeBusyResponse + ?? []; + const responses = Array.isArray(freeBusyArray) + ? freeBusyArray + : [freeBusyArray]; const allBusy: { start: Date; end: Date }[] = []; const attendeeInfo: any[] = []; @@ -114,8 +147,14 @@ export function registerAvailabilityTools(server: McpServer): void { const startTime = new Date(`${startDate}T00:00:00`); const busyPeriods = parseFreeBusyString(mergedFb, startTime); const busyCount = busyPeriods.length; - const freeCount = mergedFb.split("").filter((c: string) => c === "0").length; - attendeeInfo.push({ email, busySlots: busyCount, freeSlots: freeCount }); + const freeCount = mergedFb.split("").filter((c: string) => + c === "0" + ).length; + attendeeInfo.push({ + email, + busySlots: busyCount, + freeSlots: freeCount, + }); allBusy.push(...busyPeriods); } else { const calEvents = fbView?.CalendarEventArray?.CalendarEvent ?? []; @@ -125,7 +164,10 @@ export function registerAvailabilityTools(server: McpServer): void { const startStr = ev.StartTime ?? ""; const endStr = ev.EndTime ?? ""; if (startStr && endStr) { - allBusy.push({ start: new Date(startStr), end: new Date(endStr) }); + allBusy.push({ + start: new Date(startStr), + end: new Date(endStr), + }); } } } @@ -184,10 +226,14 @@ function parseFreeBusyString( return periods; } -function mergeBusyPeriods(periods: { start: Date; end: Date }[]): { start: Date; end: Date }[] { +function mergeBusyPeriods( + periods: { start: Date; end: Date }[], +): { start: Date; end: Date }[] { if (!periods.length) return []; - const sorted = [...periods].sort((a, b) => a.start.getTime() - b.start.getTime()); + const sorted = [...periods].sort((a, b) => + a.start.getTime() - b.start.getTime() + ); const merged: { start: Date; end: Date }[] = [{ ...sorted[0] }]; for (let i = 1; i < sorted.length; i++) { @@ -210,7 +256,10 @@ function findFreeSlots( endHour: number, durationMinutes: number, ): Record { - const result: Record = {}; + const result: Record< + string, + { start: string; end: string; durationMinutes: number }[] + > = {}; const current = new Date(startDate); current.setDate(current.getDate()); @@ -237,7 +286,8 @@ function findFreeSlots( const merged = mergeBusyPeriods(dayBusy); - const slots: { start: string; end: string; durationMinutes: number }[] = []; + const slots: { start: string; end: string; durationMinutes: number }[] = + []; let cursor = new Date(dayStart); for (const bp of merged) { diff --git a/tools/calendar.ts b/tools/calendar.ts index 84883a9..6ed2cd6 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -1,7 +1,7 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod/v4"; 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 { @@ -34,10 +34,13 @@ export function registerCalendarTools(server: McpServer): void { 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, + isAllDay: item.IsAllDayEvent === "true" + || item.IsAllDayEvent === true, + isCancelled: item.IsCancelled === "true" + || item.IsCancelled === true, isMeeting: true, - isRecurring: item.IsRecurring === "true" || item.IsRecurring === true, + isRecurring: item.IsRecurring === "true" + || item.IsRecurring === true, organizer: "", organizerEmail: "", myResponse: item.MyResponseType ?? "", @@ -61,7 +64,10 @@ export function registerCalendarTools(server: McpServer): void { } return { - content: [{ type: "text" as const, text: JSON.stringify({ events, count: events.length }) }], + content: [{ + type: "text" as const, + text: JSON.stringify({ events, count: events.length }), + }], }; } catch (error: any) { return { @@ -77,9 +83,12 @@ export function registerCalendarTools(server: McpServer): void { server.registerTool( "download_event_attachments", { - description: "Download all file attachments from a calendar event to disk", + 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"), + 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)", ), @@ -130,7 +139,9 @@ export function registerCalendarTools(server: McpServer): void { let counter = 1; let finalName = filename; while (usedNames.has(finalName.toLowerCase())) { - finalName = extPart ? `${namePart}_${counter}${extPart}` : `${namePart}_${counter}`; + finalName = extPart + ? `${namePart}_${counter}${extPart}` + : `${namePart}_${counter}`; counter++; } usedNames.add(finalName.toLowerCase()); @@ -145,7 +156,10 @@ export function registerCalendarTools(server: McpServer): void { contentType: result.contentType, }); } catch (e: any) { - errors.push({ name: att.name || "unknown", error: e.message ?? String(e) }); + errors.push({ + name: att.name || "unknown", + error: e.message ?? String(e), + }); } } diff --git a/tools/email.ts b/tools/email.ts index 3730c45..469aa5d 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -1,7 +1,7 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod/v4"; import fs from "node:fs"; import path from "node:path"; +import { z } from "zod/v4"; import { EwsClient, loadConfig } from "../ews_client.ts"; function getClient(): EwsClient { @@ -293,8 +293,12 @@ export function registerEmailTools(server: McpServer): void { { description: "Mark one or more emails as read or unread", inputSchema: z.object({ - itemIds: z.array(z.string()).describe("List of Exchange ItemIds to update"), - isRead: z.boolean().default(true).describe("True to mark as read, False to mark as unread"), + itemIds: z.array(z.string()).describe( + "List of Exchange ItemIds to update", + ), + isRead: z.boolean().default(true).describe( + "True to mark as read, False to mark as unread", + ), }), }, async ({ itemIds, isRead }) => { @@ -310,7 +314,10 @@ export function registerEmailTools(server: McpServer): void { if (errors.length) { return { - content: [{ type: "text" as const, text: JSON.stringify({ error: errors.join("; ") }) }], + content: [{ + type: "text" as const, + text: JSON.stringify({ error: errors.join("; ") }), + }], }; } @@ -391,7 +398,9 @@ export function registerEmailTools(server: McpServer): void { let counter = 1; let finalName = filename; while (usedNames.has(finalName.toLowerCase())) { - finalName = extPart ? `${namePart}_${counter}${extPart}` : `${namePart}_${counter}`; + finalName = extPart + ? `${namePart}_${counter}${extPart}` + : `${namePart}_${counter}`; counter++; } usedNames.add(finalName.toLowerCase()); @@ -406,7 +415,10 @@ export function registerEmailTools(server: McpServer): void { contentType: result.contentType, }); } catch (e: any) { - errors.push({ name: att.name || "unknown", error: e.message ?? String(e) }); + errors.push({ + name: att.name || "unknown", + error: e.message ?? String(e), + }); } } diff --git a/tools/people.ts b/tools/people.ts index edbccf0..b3c411f 100644 --- a/tools/people.ts +++ b/tools/people.ts @@ -52,7 +52,8 @@ export function registerPeopleTools(server: McpServer): void { const addrList = Array.isArray(addrs) ? addrs : [addrs]; for (const a of addrList) { if (a?.Key === "Business") { - const parts = [a.Street, a.City, a.PostalCode, a.CountryOrRegion].filter(Boolean); + const parts = [a.Street, a.City, a.PostalCode, a.CountryOrRegion] + .filter(Boolean); if (parts.length) { person.address = parts.join(", "); }