From 17a6f91b3f062975faa8e3dd0319e966a2efda78 Mon Sep 17 00:00:00 2001 From: albnnc Date: Fri, 10 Jul 2026 16:36:22 +0300 Subject: [PATCH 1/8] w --- SKILL.md | 179 ------------------------------------------ tools/auth.ts | 23 +++--- tools/availability.ts | 22 +++--- tools/calendar.ts | 9 ++- tools/email.ts | 32 +++++--- tools/folders.ts | 7 +- tools/people.ts | 3 +- 7 files changed, 56 insertions(+), 219 deletions(-) delete mode 100644 SKILL.md diff --git a/SKILL.md b/SKILL.md deleted file mode 100644 index 3ea570b..0000000 --- a/SKILL.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -name: exchange-mcp -description: >- - Use this skill when the user talks about - email, calendar, contacts, Exchange, EWS, corporate mail, - or scheduling meetings. ---- - -# exchange-mcp — MCP server for Microsoft Exchange - -This MCP server provides access to **Microsoft Exchange Server** via **Exchange -Web Services (EWS)** with NTLM authentication. Supports email, calendar, -contacts, and free/busy scheduling. - -**Use this server when the user talks about:** - -- Email, inbox, sent items, drafts -- Calendar, events, meetings, availability -- Contacts, people, colleagues, employees -- Attachments, files in emails -- Exchange, EWS, corporate mail - -**Does not support:** Microsoft 365 / Exchange Online (OAuth), IMAP/POP3/SMTP, -Gmail, Outlook.com. - ---- - -## Tools - -### Auth - -#### `login` - -Authenticate to Exchange EWS via NTLM. On first use, all fields except `domain` -are required. On subsequent logins, only `password` is needed — previously saved -`serverUrl`, `email`, `username`, and `domain` are reused automatically. - -If the user asks to log in and only provides a password, this is likely a -_repeated_ login — the previously saved config will be reused automatically. -Just call the `login` tool with the password alone. - -| Parameter | Type | Required | Description | -| ----------- | ------ | -------- | ------------------------------------------------ | -| `serverUrl` | string | no | EWS server URL (e.g. `https://mail.example.com`) | -| `email` | string | no | Email address | -| `username` | string | no | NTLM username | -| `password` | string | yes | NTLM password | -| `domain` | string | no | NTLM domain (default: `corp`) | - -#### `check_session` - -Check if the current session is authenticated. No parameters. Returns -`{authenticated, email, serverUrl}`. - -#### `logout` - -Clear stored credentials. No parameters. - ---- - -### Email - -#### `get_emails` - -Get emails from a folder. Supports optional text search via -`query`/`queryScope`. - -| Parameter | Type | Default | Description | -| ------------- | ------- | --------- | ----------------------------------------------------------------------------------- | -| `folder` | string | `"Inbox"` | Folder name (Inbox, Sent, Drafts, Deleted, Junk, or custom; supports Russian names) | -| `limit` | number | `10` | Max emails (max 50; max 500 if `idsOnly=true`) | -| `offset` | number | `0` | Pagination offset | -| `includeBody` | boolean | `false` | If `true`, fetches full email body | -| `unreadOnly` | boolean | `false` | Only unread emails | -| `idsOnly` | boolean | `false` | Return only IDs + dates + subjects (faster, higher limit) | -| `query` | string | — | Text to search for within the folder | -| `queryScope` | enum | `"all"` | Scope: `all`, `subject`, `body`, `from` (only when `query` is set) | - -#### `get_email` - -Get a single email with full body and details. - -| Parameter | Type | Required | Description | -| --------- | ------ | -------- | ---------------------------- | -| `itemId` | string | yes | Exchange ItemId of the email | - -#### `mark_email_read` - -Mark emails as read or unread. - -| Parameter | Type | Default | Description | -| --------- | -------- | ------- | ------------------------------- | -| `itemIds` | string[] | — | List of Exchange ItemIds | -| `isRead` | boolean | `true` | `true` = read, `false` = unread | - -#### `download_attachments` - -Download all file attachments from an email to disk. - -| Parameter | Type | Default | Description | -| -------------- | ------ | -------------------- | --------------- | -| `itemId` | string | — | Exchange ItemId | -| `targetFolder` | string | `"/tmp/attachments"` | Local folder | - ---- - -### Folders - -#### `get_folders` - -List mailbox folders. - -| Parameter | Type | Default | Description | -| ---------------- | ------- | ----------------- | ----------------------------------------------------------------- | -| `parentFolderId` | string | `"msgfolderroot"` | Parent folder (supports distinguished names: `inbox`, `calendar`) | -| `recursive` | boolean | `false` | Recursively traverse subfolders | - ---- - -### Calendar - -#### `get_calendar_events` - -Get calendar events within a date range. - -| Parameter | Type | Required | Description | -| ------------- | ------- | -------- | ----------------------------------------- | -| `startDate` | string | yes | Start (`YYYY-MM-DD`) | -| `endDate` | string | yes | End (`YYYY-MM-DD`) | -| `includeBody` | boolean | `false` | Full details (organizer, attendees, body) | - -#### `download_event_attachments` - -Download attachments from a calendar event. Same parameters as -`download_attachments`. - ---- - -### Availability - -#### `find_free_time` - -Find free time slots in your calendar. - -| Parameter | Type | Default | Description | -| ----------------- | ------ | ----------- | -------------------------- | -| `startDate` | string | — | Start (`YYYY-MM-DD`) | -| `endDate` | string | = startDate | End | -| `durationMinutes` | number | `30` | Minimum slot duration | -| `startHour` | number | `9` | Work day start hour (0-23) | -| `endHour` | number | `18` | Work day end hour (0-23) | - -#### `find_meeting_time` - -Find common free time for multiple people. - -| Parameter | Type | Default | Description | -| ----------------- | ------ | ----------- | --------------------------------- | -| `emails` | string | — | Attendee emails (comma-separated) | -| `startDate` | string | — | Start | -| `endDate` | string | = startDate | End | -| `durationMinutes` | number | `30` | Minimum slot duration | -| `startHour` | number | `9` | Work day start hour | -| `endHour` | number | `18` | Work day end hour | - ---- - -### People - -#### `find_person` - -Search for people in the corporate directory (Active Directory). - -| Parameter | Type | Required | Description | -| --------- | ------ | -------- | ----------------------- | -| `query` | string | yes | Name, email, or keyword | - -Returns: name, email, job title, department, company, office, phone, manager, -direct reports. diff --git a/tools/auth.ts b/tools/auth.ts index df23bc1..922317a 100644 --- a/tools/auth.ts +++ b/tools/auth.ts @@ -14,15 +14,17 @@ export function registerAuthTools(server: McpServer): void { server.registerTool( "login", { - description: "Authenticate to Exchange EWS using NTLM credentials", + description: "Authenticate to Exchange EWS using NTLM credentials." + + " On first use all fields except domain are required." + + " On subsequent logins only password is needed — previously saved" + + " serverUrl, email, username, and domain are reused automatically from config." + + " Returns {success, message} on success or {success, error} on failure.", inputSchema: z.object({ - serverUrl: z.string().optional().describe( - "EWS server URL (e.g. https://mail.example.com)", - ), + serverUrl: z.string().optional().describe("Server URL"), email: z.string().optional().describe("Email address"), username: z.string().optional().describe("NTLM username"), password: z.string().describe("NTLM password"), - domain: z.string().optional().describe("NTLM domain (default: corp)"), + domain: z.string().optional().describe("NTLM domain"), }), }, async ({ serverUrl, email, username, password, domain }) => { @@ -47,8 +49,9 @@ export function registerAuthTools(server: McpServer): void { type: "text" as const, text: JSON.stringify({ success: false, - error: - "Missing required fields. Provide serverUrl, email, and username, or login once with all fields first.", + error: "Missing required fields." + + " Provide serverUrl, email, and username, or" + + " login once with all fields first.", }), }], }; @@ -99,7 +102,8 @@ export function registerAuthTools(server: McpServer): void { server.registerTool( "check_session", { - description: "Check whether the current EWS session is authenticated", + description: + "Check whether the current EWS session is authenticated. No parameters. Returns {authenticated, email, serverUrl} or {authenticated, error}.", inputSchema: z.object({}), }, async () => { @@ -147,7 +151,8 @@ export function registerAuthTools(server: McpServer): void { server.registerTool( "logout", { - description: "Clear stored credentials", + description: + "Clear stored credentials (serverUrl, email, username, domain, password). No parameters. Returns {success, message}.", inputSchema: z.object({}), }, async () => { diff --git a/tools/availability.ts b/tools/availability.ts index d412177..117f9ce 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -6,20 +6,21 @@ export function registerAvailabilityTools(server: McpServer): void { server.registerTool( "find_free_time", { - description: "Find free time slots in your own calendar", + description: + "Find free time slots in your own calendar. Queries your free/busy status and returns available slots within working hours that meet the minimum duration. Returns {freeSlots: {date: [{start, end, durationMinutes}]}}. Defaults: durationMinutes=30, startHour=9, endHour=18.", 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)", + "End date in YYYY-MM-DD format. Defaults to startDate", ), durationMinutes: z.number().default(30).describe( - "Minimum slot duration in minutes (default 30)", + "Minimum slot duration in minutes. Default: 30", ), startHour: z.number().default(9).describe( - "Working day start hour (0-23, default 9)", + "Working day start hour (0-23). Default: 9", ), endHour: z.number().default(18).describe( - "Working day end hour (0-23, default 18)", + "Working day end hour (0-23). Default: 18", ), }), }, @@ -94,23 +95,24 @@ export function registerAvailabilityTools(server: McpServer): void { server.registerTool( "find_meeting_time", { - description: "Find meeting times that work for multiple people", + description: + "Find common free time for multiple attendees. Queries free/busy for all attendees and returns slots where everyone is available. Returns {period, attendees, freeSlots}. Defaults: durationMinutes=30, startHour=9, endHour=18.", inputSchema: z.object({ 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)", + "End date in YYYY-MM-DD format. Defaults to startDate", ), durationMinutes: z.number().default(30).describe( - "Minimum slot duration in minutes (default 30)", + "Minimum slot duration in minutes. Default: 30", ), startHour: z.number().default(9).describe( - "Working day start hour (0-23, default 9)", + "Working day start hour (0-23). Default: 9", ), endHour: z.number().default(18).describe( - "Working day end hour (0-23, default 18)", + "Working day end hour (0-23). Default: 18", ), }), }, diff --git a/tools/calendar.ts b/tools/calendar.ts index 6ed2cd6..2341d77 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -8,12 +8,13 @@ export function registerCalendarTools(server: McpServer): void { server.registerTool( "get_calendar_events", { - description: "Get calendar events within a date range", + 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", + "If true, fetch full event details (organizer, attendees, body) via GetItem. Default: false", ), }), }, @@ -84,13 +85,13 @@ export function registerCalendarTools(server: McpServer): void { "download_event_attachments", { description: - "Download all file attachments from a calendar event to disk", + "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)", + "Local directory to save files. Default: /tmp/attachments", ), }), }, diff --git a/tools/email.ts b/tools/email.ts index c5c6825..d507c57 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -12,31 +12,34 @@ export function registerEmailTools(server: McpServer): void { server.registerTool( "get_emails", { - description: "Get emails from a mailbox folder", + description: + "Get emails from a mailbox folder. Supports optional text search via query/queryScope. Returns {emails, count} or {itemIds, count} if idsOnly. Defaults: folder=Inbox, limit=10, offset=0, includeBody=false, unreadOnly=false, idsOnly=false, queryScope=all. Note: idsOnly raises the limit to 500.", inputSchema: z.object({ folder: z.string().default("Inbox").describe( - "Folder name (Inbox, Sent, Drafts, Deleted, Junk, or custom)", + "Folder name (Inbox, Sent, Drafts, Deleted, Junk, or custom). Supports Russian folder names", ), limit: z.number().default(10).describe( - "Maximum number of emails to return (default 10, max 50)", + "Maximum number of emails to return. Default: 10. Max 50 (max 500 if idsOnly=true)", ), offset: z.number().default(0).describe( - "Number of emails to skip for pagination", + "Number of emails to skip for pagination. Default: 0", ), includeBody: z.boolean().default(false).describe( - "If True, fetch full body for each email (slower)", + "If true, fetches full email body for each email (slower). Default: false", ), unreadOnly: z.boolean().default(false).describe( - "If True, only return unread emails", + "If true, only return unread emails. Default: false", ), idsOnly: z.boolean().default(false).describe( - "If True, return only item IDs and dates (max limit 500)", + "If true, return only item IDs, dates, and subjects — faster with higher limit of 500. Default: false", ), query: z.string().optional().describe( "Optional text to search for within the folder", ), queryScope: z.enum(["all", "subject", "body", "from"]).default("all") - .describe("Where to search (only used when query is set)"), + .describe( + "Scope for text search. Values: all (subject+body), subject, body, from. Only used when query is set. Default: all", + ), }), }, async ( @@ -177,7 +180,8 @@ export function registerEmailTools(server: McpServer): void { server.registerTool( "get_email", { - description: "Get a single email with full body and details", + description: + "Get a single email with full body and details by Exchange ItemId. Returns the full Email object (subject, from, to, cc, body, date, attachments, etc.).", inputSchema: z.object({ itemId: z.string().describe( "The Exchange ItemId of the email to retrieve", @@ -207,13 +211,14 @@ export function registerEmailTools(server: McpServer): void { server.registerTool( "mark_email_read", { - description: "Mark one or more emails as read or unread", + description: + "Mark one or more emails as read or unread by their Exchange ItemIds. Returns {success, message} or {error}.", 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", + "True to mark as read, false to mark as unread. Default: true", ), }), }, @@ -261,11 +266,12 @@ export function registerEmailTools(server: McpServer): void { server.registerTool( "download_attachments", { - description: "Download all file attachments from an email to disk", + description: + "Download all file attachments from an email 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 email"), targetFolder: z.string().default("/tmp/attachments").describe( - "Local directory to save files (default /tmp/attachments)", + "Local directory to save files. Default: /tmp/attachments", ), }), }, diff --git a/tools/folders.ts b/tools/folders.ts index 1615a65..a00789f 100644 --- a/tools/folders.ts +++ b/tools/folders.ts @@ -6,13 +6,14 @@ export function registerFolderTools(server: McpServer): void { server.registerTool( "get_folders", { - description: "List mail folders from the Exchange mailbox", + description: + "List mailbox folders from the Exchange mailbox. Returns a list of Folder objects (name, id, totalCount, unreadCount, childFolderCount).", inputSchema: z.object({ parentFolderId: z.string().default("msgfolderroot").describe( - "Parent folder to list children of (default: msgfolderroot)", + "Parent folder to list children of. Supports distinguished names (e.g. inbox, calendar). Default: msgfolderroot", ), recursive: z.boolean().default(false).describe( - "If True, traverse all subfolders recursively", + "If true, recursively traverse all subfolders. Default: false", ), }), }, diff --git a/tools/people.ts b/tools/people.ts index b3c411f..72f684e 100644 --- a/tools/people.ts +++ b/tools/people.ts @@ -6,7 +6,8 @@ export function registerPeopleTools(server: McpServer): void { server.registerTool( "find_person", { - description: "Search for people in the corporate directory", + description: + "Search for people in the corporate directory (Active Directory) by name, email, or keyword. Returns a list of Person objects with name, email, jobTitle, department, company, office, phones, manager, directReports, and more.", inputSchema: z.object({ query: z.string().describe( "Name, email address, or keyword to search for", -- 2.52.0 From 1599bf39815790ed31acfdf5407aab61b64713d1 Mon Sep 17 00:00:00 2001 From: albnnc Date: Fri, 10 Jul 2026 16:38:28 +0300 Subject: [PATCH 2/8] w --- tools/availability.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/availability.ts b/tools/availability.ts index 117f9ce..2b5909c 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -6,8 +6,11 @@ export function registerAvailabilityTools(server: McpServer): void { server.registerTool( "find_free_time", { - description: - "Find free time slots in your own calendar. Queries your free/busy status and returns available slots within working hours that meet the minimum duration. Returns {freeSlots: {date: [{start, end, durationMinutes}]}}. Defaults: durationMinutes=30, startHour=9, endHour=18.", + description: "Find free time slots in your own calendar." + + " Queries your free/busy status and returns available slots" + + " within working hours that meet the minimum duration." + + " Returns {freeSlots: {date: [{start, end, durationMinutes}]}}." + + " Defaults: durationMinutes=30, startHour=9, endHour=18.", inputSchema: z.object({ startDate: z.string().describe("Start date in YYYY-MM-DD format"), endDate: z.string().optional().describe( -- 2.52.0 From 979e9745217d9561d5db177a25b5d668cf9863da Mon Sep 17 00:00:00 2001 From: albnnc Date: Fri, 10 Jul 2026 16:47:35 +0300 Subject: [PATCH 3/8] w --- tools/auth.ts | 42 +++++++++++-- tools/availability.ts | 60 ++++++++++++++---- tools/calendar.ts | 56 +++++++++++++++-- tools/email.ts | 140 +++++++++++++++++++++++++++++------------- tools/folders.ts | 17 +++-- tools/people.ts | 25 +++++++- 6 files changed, 268 insertions(+), 72 deletions(-) diff --git a/tools/auth.ts b/tools/auth.ts index 922317a..3816082 100644 --- a/tools/auth.ts +++ b/tools/auth.ts @@ -17,15 +17,19 @@ export function registerAuthTools(server: McpServer): void { description: "Authenticate to Exchange EWS using NTLM credentials." + " On first use all fields except domain are required." + " On subsequent logins only password is needed — previously saved" - + " serverUrl, email, username, and domain are reused automatically from config." - + " Returns {success, message} on success or {success, error} on failure.", + + " serverUrl, email, username, and domain are reused automatically from config.", inputSchema: z.object({ - serverUrl: z.string().optional().describe("Server URL"), + serverUrl: z.string().optional().describe("EWS server URL"), email: z.string().optional().describe("Email address"), username: z.string().optional().describe("NTLM username"), password: z.string().describe("NTLM password"), domain: z.string().optional().describe("NTLM domain"), }), + outputSchema: z.object({ + success: z.boolean(), + message: z.string().optional(), + error: z.string().optional(), + }), }, async ({ serverUrl, email, username, password, domain }) => { try { @@ -103,8 +107,14 @@ export function registerAuthTools(server: McpServer): void { "check_session", { description: - "Check whether the current EWS session is authenticated. No parameters. Returns {authenticated, email, serverUrl} or {authenticated, error}.", + "Check whether the current EWS session is authenticated. No parameters.", inputSchema: z.object({}), + outputSchema: z.object({ + authenticated: z.boolean(), + email: z.string().optional(), + serverUrl: z.string().optional(), + error: z.string().optional(), + }), }, async () => { try { @@ -117,6 +127,10 @@ export function registerAuthTools(server: McpServer): void { error: "Not logged in. Password not found in memory.", }), }], + structuredContent: { + authenticated: false, + error: "Not logged in. Password not found in memory.", + }, }; } const config = loadConfig(); @@ -133,6 +147,12 @@ export function registerAuthTools(server: McpServer): void { error: result.error, }), }], + structuredContent: { + authenticated: result.ok, + email: config.email, + serverUrl: config.serverUrl, + error: result.error, + }, }; } catch (error: any) { return { @@ -143,6 +163,10 @@ export function registerAuthTools(server: McpServer): void { error: error.message ?? String(error), }), }], + structuredContent: { + authenticated: false, + error: error.message ?? String(error), + }, }; } }, @@ -152,8 +176,12 @@ export function registerAuthTools(server: McpServer): void { "logout", { description: - "Clear stored credentials (serverUrl, email, username, domain, password). No parameters. Returns {success, message}.", + "Clear stored credentials (serverUrl, email, username, domain, password). No parameters.", inputSchema: z.object({}), + outputSchema: z.object({ + success: z.boolean(), + message: z.string().optional(), + }), }, async () => { clearConfig(); @@ -165,6 +193,10 @@ export function registerAuthTools(server: McpServer): void { message: "Logged out. Credentials cleared.", }), }], + structuredContent: { + success: true, + message: "Logged out. Credentials cleared.", + }, }; }, ); diff --git a/tools/availability.ts b/tools/availability.ts index 2b5909c..e8c1f56 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -8,22 +8,30 @@ export function registerAvailabilityTools(server: McpServer): void { { description: "Find free time slots in your own calendar." + " Queries your free/busy status and returns available slots" - + " within working hours that meet the minimum duration." - + " Returns {freeSlots: {date: [{start, end, durationMinutes}]}}." - + " Defaults: durationMinutes=30, startHour=9, endHour=18.", + + " within working hours that meet the minimum duration.", 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", + "End date in YYYY-MM-DD format", ), durationMinutes: z.number().default(30).describe( - "Minimum slot duration in minutes. Default: 30", + "Minimum slot duration in minutes", ), startHour: z.number().default(9).describe( - "Working day start hour (0-23). Default: 9", + "Working day start hour (0-23)", ), endHour: z.number().default(18).describe( - "Working day end hour (0-23). Default: 18", + "Working day end hour (0-23)", + ), + }), + outputSchema: z.object({ + freeSlots: z.record( + z.string(), + z.array(z.object({ + start: z.string(), + end: z.string(), + durationMinutes: z.number(), + })), ), }), }, @@ -83,6 +91,7 @@ export function registerAvailabilityTools(server: McpServer): void { type: "text" as const, text: JSON.stringify({ freeSlots }), }], + structuredContent: { freeSlots }, }; } catch (error: any) { return { @@ -98,24 +107,44 @@ export function registerAvailabilityTools(server: McpServer): void { server.registerTool( "find_meeting_time", { - description: - "Find common free time for multiple attendees. Queries free/busy for all attendees and returns slots where everyone is available. Returns {period, attendees, freeSlots}. Defaults: durationMinutes=30, startHour=9, endHour=18.", + description: "Find common free time for multiple attendees." + + " Queries free/busy for all attendees and returns slots where everyone is available.", inputSchema: z.object({ 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", + "End date in YYYY-MM-DD format", ), durationMinutes: z.number().default(30).describe( - "Minimum slot duration in minutes. Default: 30", + "Minimum slot duration in minutes", ), startHour: z.number().default(9).describe( - "Working day start hour (0-23). Default: 9", + "Working day start hour (0-23)", ), endHour: z.number().default(18).describe( - "Working day end hour (0-23). Default: 18", + "Working day end hour (0-23)", + ), + }), + outputSchema: z.object({ + period: z.object({ + start: z.string(), + end: z.string(), + }), + attendees: z.array(z.object({ + email: z.string(), + busySlots: z.number().optional(), + freeSlots: z.number().optional(), + calendarEvents: z.number().optional(), + })), + freeSlots: z.record( + z.string(), + z.array(z.object({ + start: z.string(), + end: z.string(), + durationMinutes: z.number(), + })), ), }), }, @@ -197,6 +226,11 @@ export function registerAvailabilityTools(server: McpServer): void { freeSlots: freeByDate, }), }], + structuredContent: { + period: { start: startDate, end: ed }, + attendees: attendeeInfo, + freeSlots: freeByDate, + }, }; } catch (error: any) { return { diff --git a/tools/calendar.ts b/tools/calendar.ts index 2341d77..4850ec4 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -8,15 +8,35 @@ 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}.", + description: "Get calendar events within a date range." + + " Set includeBody=true to fetch organizer, attendees, and body via GetItem.", 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", + "If true, fetch full event details (organizer, attendees, body) via GetItem", ), }), + outputSchema: z.object({ + events: z.array(z.object({ + subject: z.string(), + start: z.string(), + end: z.string(), + location: z.string(), + isAllDay: z.boolean(), + isCancelled: z.boolean(), + isMeeting: z.boolean(), + isRecurring: z.boolean(), + organizer: z.string(), + organizerEmail: z.string(), + myResponse: z.string(), + itemId: z.string(), + body: z.string(), + requiredAttendees: z.array(z.string()), + optionalAttendees: z.array(z.string()), + })), + count: z.number(), + }), }, async ({ startDate, endDate, includeBody }) => { try { @@ -69,6 +89,7 @@ export function registerCalendarTools(server: McpServer): void { type: "text" as const, text: JSON.stringify({ events, count: events.length }), }], + structuredContent: { events, count: events.length }, }; } catch (error: any) { return { @@ -85,15 +106,31 @@ export function registerCalendarTools(server: McpServer): void { "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}.", + "Download all file attachments from a calendar event to disk." + + " Inline (embedded) attachments are skipped — only standalone file attachments are downloaded.", 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", + "Local directory to save files", ), }), + outputSchema: z.object({ + success: z.boolean(), + downloaded: z.array(z.object({ + name: z.string(), + path: z.string(), + size: z.number(), + contentType: z.string(), + })), + count: z.number(), + errors: z.array(z.object({ + name: z.string(), + error: z.string(), + })).optional(), + message: z.string().optional(), + }), }, async ({ itemId, targetFolder }) => { try { @@ -117,6 +154,12 @@ export function registerCalendarTools(server: McpServer): void { message: "No downloadable file attachments.", }), }], + structuredContent: { + success: true, + downloaded: [], + count: 0, + message: "No downloadable file attachments.", + }, }; } @@ -174,6 +217,9 @@ export function registerCalendarTools(server: McpServer): void { ...(errors.length ? { errors } : {}), }), }], + structuredContent: errors.length === 0 + ? { success: true, downloaded, count: downloaded.length } + : { success: false, downloaded, count: downloaded.length, errors }, }; } catch (error: any) { return { diff --git a/tools/email.ts b/tools/email.ts index d507c57..7dbce39 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -12,34 +12,40 @@ export function registerEmailTools(server: McpServer): void { server.registerTool( "get_emails", { - description: - "Get emails from a mailbox folder. Supports optional text search via query/queryScope. Returns {emails, count} or {itemIds, count} if idsOnly. Defaults: folder=Inbox, limit=10, offset=0, includeBody=false, unreadOnly=false, idsOnly=false, queryScope=all. Note: idsOnly raises the limit to 500.", + description: "Get emails from a mailbox folder." + + " Supports optional text search via query/queryScope.", inputSchema: z.object({ folder: z.string().default("Inbox").describe( "Folder name (Inbox, Sent, Drafts, Deleted, Junk, or custom). Supports Russian folder names", ), limit: z.number().default(10).describe( - "Maximum number of emails to return. Default: 10. Max 50 (max 500 if idsOnly=true)", + "Maximum number of emails to return", ), offset: z.number().default(0).describe( - "Number of emails to skip for pagination. Default: 0", + "Number of emails to skip for pagination", ), includeBody: z.boolean().default(false).describe( - "If true, fetches full email body for each email (slower). Default: false", + "If true, fetches full email body for each email (slower)", ), unreadOnly: z.boolean().default(false).describe( - "If true, only return unread emails. Default: false", - ), - idsOnly: z.boolean().default(false).describe( - "If true, return only item IDs, dates, and subjects — faster with higher limit of 500. Default: false", + "If true, only return unread emails", ), query: z.string().optional().describe( "Optional text to search for within the folder", ), queryScope: z.enum(["all", "subject", "body", "from"]).default("all") - .describe( - "Scope for text search. Values: all (subject+body), subject, body, from. Only used when query is set. Default: all", - ), + .describe("Search scope: all, subject, body, or from"), + }), + outputSchema: z.object({ + emails: z.array(z.object({ + itemId: z.string(), + subject: z.string(), + from: z.string(), + date: z.string(), + isRead: z.boolean(), + hasAttachments: z.boolean(), + })), + count: z.number(), }), }, async ( @@ -49,19 +55,18 @@ export function registerEmailTools(server: McpServer): void { offset, includeBody, unreadOnly, - idsOnly, query, queryScope, }, ) => { try { const client = getClient(); - const maxLimit = idsOnly ? 500 : 50; + const maxLimit = 50; if (limit > maxLimit) limit = maxLimit; offset = Math.max(0, offset); const folderId = await client.getFolderId(folder); - const baseShape = idsOnly ? "IdOnly" : "AllProperties"; + const baseShape = "AllProperties"; if (query && !query.trim()) query = undefined; @@ -112,16 +117,16 @@ export function registerEmailTools(server: McpServer): void { let restriction = ""; if (restrictions.length === 1) { restriction = `\ - - ${restrictions[0]} - `; + + ${restrictions[0]} + `; } else if (restrictions.length > 1) { restriction = `\ - - - ${restrictions.join("\n")} - - `; + + + ${restrictions.join("\n")} + + `; } const items = await client.findItems(folderId, { @@ -131,20 +136,6 @@ export function registerEmailTools(server: McpServer): void { restriction, }); - if (idsOnly) { - const result = items.map((item: any) => ({ - itemId: item.ItemId?.["@_Id"] ?? "", - date: item.DateTimeReceived ?? "", - subject: item.Subject ?? "", - })); - return { - content: [{ - type: "text" as const, - text: JSON.stringify({ itemIds: result, count: result.length }), - }], - }; - } - const emails = []; for (const item of items) { const email = client.extractEmailSummary(item); @@ -165,6 +156,7 @@ export function registerEmailTools(server: McpServer): void { type: "text" as const, text: JSON.stringify({ emails, count: emails.length }), }], + structuredContent: { emails, count: emails.length }, }; } catch (error: any) { return { @@ -181,12 +173,32 @@ export function registerEmailTools(server: McpServer): void { "get_email", { description: - "Get a single email with full body and details by Exchange ItemId. Returns the full Email object (subject, from, to, cc, body, date, attachments, etc.).", + "Get a single email with full body and details by Exchange ItemId.", inputSchema: z.object({ itemId: z.string().describe( "The Exchange ItemId of the email to retrieve", ), }), + outputSchema: z.object({ + itemId: z.string(), + subject: z.string(), + from: z.string(), + fromName: z.string().optional(), + to: z.array(z.string()).optional(), + cc: z.array(z.string()).optional(), + date: z.string(), + body: z.string().optional(), + isRead: z.boolean().optional(), + hasAttachments: z.boolean().optional(), + hasLinks: z.boolean().optional(), + attachments: z.array(z.object({ + name: z.string(), + size: z.number(), + contentType: z.string(), + attachmentId: z.string().optional(), + isInline: z.boolean().optional(), + })).optional(), + }), }, async ({ itemId }) => { try { @@ -196,6 +208,7 @@ export function registerEmailTools(server: McpServer): void { return { content: [{ type: "text" as const, text: JSON.stringify(email) }], + structuredContent: { emails: [email] }, }; } catch (error: any) { return { @@ -212,15 +225,20 @@ export function registerEmailTools(server: McpServer): void { "mark_email_read", { description: - "Mark one or more emails as read or unread by their Exchange ItemIds. Returns {success, message} or {error}.", + "Mark one or more emails as read or unread by their Exchange ItemIds.", 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. Default: true", + "True to mark as read, false to mark as unread", ), }), + outputSchema: z.object({ + success: z.boolean(), + message: z.string().optional(), + error: z.string().optional(), + }), }, async ({ itemIds, isRead }) => { try { @@ -239,6 +257,10 @@ export function registerEmailTools(server: McpServer): void { type: "text" as const, text: JSON.stringify({ error: errors.join("; ") }), }], + structuredContent: { + success: false, + error: errors.join("; "), + }, }; } @@ -251,6 +273,10 @@ export function registerEmailTools(server: McpServer): void { message: `Marked ${itemIds.length} email(s) as ${status}.`, }), }], + structuredContent: { + success: true, + message: `Marked ${itemIds.length} email(s) as ${status}.`, + }, }; } catch (error: any) { return { @@ -258,6 +284,10 @@ export function registerEmailTools(server: McpServer): void { type: "text" as const, text: JSON.stringify({ error: error.message ?? String(error) }), }], + structuredContent: { + success: false, + error: error.message ?? String(error), + }, }; } }, @@ -266,14 +296,29 @@ export function registerEmailTools(server: McpServer): void { server.registerTool( "download_attachments", { - description: - "Download all file attachments from an email to disk. Inline (embedded) attachments are skipped — only standalone file attachments are downloaded. Returns {success, downloaded, count} or {success, downloaded, count, errors}.", + description: "Download all file attachments from an email to disk." + + " Inline (embedded) attachments are skipped — only standalone file attachments are downloaded.", inputSchema: z.object({ itemId: z.string().describe("The Exchange ItemId of the email"), targetFolder: z.string().default("/tmp/attachments").describe( - "Local directory to save files. Default: /tmp/attachments", + "Local directory to save files", ), }), + outputSchema: z.object({ + success: z.boolean(), + downloaded: z.array(z.object({ + name: z.string(), + path: z.string(), + size: z.number(), + contentType: z.string(), + })), + count: z.number(), + errors: z.array(z.object({ + name: z.string(), + error: z.string(), + })).optional(), + message: z.string().optional(), + }), }, async ({ itemId, targetFolder }) => { try { @@ -297,6 +342,12 @@ export function registerEmailTools(server: McpServer): void { message: "No downloadable file attachments.", }), }], + structuredContent: { + success: true, + downloaded: [], + count: 0, + message: "No downloadable file attachments.", + }, }; } @@ -354,6 +405,9 @@ export function registerEmailTools(server: McpServer): void { ...(errors.length ? { errors } : {}), }), }], + structuredContent: errors.length === 0 + ? { success: true, downloaded, count: downloaded.length } + : { success: false, downloaded, count: downloaded.length, errors }, }; } catch (error: any) { return { diff --git a/tools/folders.ts b/tools/folders.ts index a00789f..36974b5 100644 --- a/tools/folders.ts +++ b/tools/folders.ts @@ -6,16 +6,24 @@ export function registerFolderTools(server: McpServer): void { server.registerTool( "get_folders", { - description: - "List mailbox folders from the Exchange mailbox. Returns a list of Folder objects (name, id, totalCount, unreadCount, childFolderCount).", + description: "List mailbox folders from the Exchange mailbox.", inputSchema: z.object({ parentFolderId: z.string().default("msgfolderroot").describe( - "Parent folder to list children of. Supports distinguished names (e.g. inbox, calendar). Default: msgfolderroot", + "Parent folder to list children of. Supports distinguished names (e.g. inbox, calendar)", ), recursive: z.boolean().default(false).describe( - "If true, recursively traverse all subfolders. Default: false", + "If true, recursively traverse all subfolders", ), }), + outputSchema: z.object({ + folders: z.array(z.object({ + name: z.string(), + id: z.string(), + totalCount: z.number(), + unreadCount: z.number(), + childFolderCount: z.number(), + })), + }), }, async ({ parentFolderId, recursive }) => { try { @@ -24,6 +32,7 @@ export function registerFolderTools(server: McpServer): void { return { content: [{ type: "text" as const, text: JSON.stringify(folders) }], + structuredContent: { folders }, }; } catch (error: any) { return { diff --git a/tools/people.ts b/tools/people.ts index 72f684e..b47ef7e 100644 --- a/tools/people.ts +++ b/tools/people.ts @@ -7,17 +7,37 @@ export function registerPeopleTools(server: McpServer): void { "find_person", { description: - "Search for people in the corporate directory (Active Directory) by name, email, or keyword. Returns a list of Person objects with name, email, jobTitle, department, company, office, phones, manager, directReports, and more.", + "Search for people in the corporate directory (Active Directory) by name, email, or keyword.", inputSchema: z.object({ query: z.string().describe( "Name, email address, or keyword to search for", ), }), + outputSchema: z.object({ + name: z.string(), + email: z.string(), + mailboxType: z.string(), + firstName: z.string(), + lastName: z.string(), + jobTitle: z.string(), + department: z.string(), + company: z.string(), + office: z.string(), + alias: z.string(), + manager: z.string(), + managerEmail: z.string(), + phones: z.record(z.string(), z.string()), + address: z.string(), + directReports: z.array(z.object({ + name: z.string(), + email: z.string(), + })), + }), }, async ({ query }) => { try { const client = new EwsClient(loadConfig()); - const resolutions = await client.resolveNames(query, true); + const resolutions = await client.resolveNames(query); const people = resolutions.map((r: any) => { const mailbox = r.Mailbox ?? {}; @@ -85,6 +105,7 @@ export function registerPeopleTools(server: McpServer): void { return { content: [{ type: "text" as const, text: JSON.stringify(people) }], + structuredContent: { people }, }; } catch (error: any) { return { -- 2.52.0 From afa2bf5126221535c825ba74c71669a2f4bdada7 Mon Sep 17 00:00:00 2001 From: albnnc Date: Fri, 10 Jul 2026 16:56:16 +0300 Subject: [PATCH 4/8] w --- tools/auth.ts | 6 ++++-- tools/availability.ts | 3 ++- tools/calendar.ts | 6 ++++-- tools/email.ts | 6 ++++-- tools/folders.ts | 3 ++- tools/people.ts | 3 ++- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tools/auth.ts b/tools/auth.ts index 3816082..a8c6308 100644 --- a/tools/auth.ts +++ b/tools/auth.ts @@ -17,7 +17,8 @@ export function registerAuthTools(server: McpServer): void { description: "Authenticate to Exchange EWS using NTLM credentials." + " On first use all fields except domain are required." + " On subsequent logins only password is needed — previously saved" - + " serverUrl, email, username, and domain are reused automatically from config.", + + " serverUrl, email, username, and domain are reused" + + " automatically from config.", inputSchema: z.object({ serverUrl: z.string().optional().describe("EWS server URL"), email: z.string().optional().describe("Email address"), @@ -176,7 +177,8 @@ export function registerAuthTools(server: McpServer): void { "logout", { description: - "Clear stored credentials (serverUrl, email, username, domain, password). No parameters.", + "Clear stored credentials (serverUrl, email, username, domain, password)." + + " No parameters.", inputSchema: z.object({}), outputSchema: z.object({ success: z.boolean(), diff --git a/tools/availability.ts b/tools/availability.ts index e8c1f56..2cd8a91 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -108,7 +108,8 @@ export function registerAvailabilityTools(server: McpServer): void { "find_meeting_time", { description: "Find common free time for multiple attendees." - + " Queries free/busy for all attendees and returns slots where everyone is available.", + + " Queries free/busy for all attendees" + + " and returns slots where everyone is available.", inputSchema: z.object({ emails: z.string().describe( "Comma-separated email addresses of attendees", diff --git a/tools/calendar.ts b/tools/calendar.ts index 4850ec4..00f18d8 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -14,7 +14,8 @@ export function registerCalendarTools(server: McpServer): void { 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", + "If true, fetch full event details (organizer, attendees, body)" + + " via GetItem", ), }), outputSchema: z.object({ @@ -107,7 +108,8 @@ export function registerCalendarTools(server: McpServer): void { { description: "Download all file attachments from a calendar event to disk." - + " Inline (embedded) attachments are skipped — only standalone file attachments are downloaded.", + + " Inline (embedded) attachments are skipped —" + + " only standalone file attachments are downloaded.", inputSchema: z.object({ itemId: z.string().describe( "The Exchange ItemId of the calendar event", diff --git a/tools/email.ts b/tools/email.ts index 7dbce39..66a9981 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -16,7 +16,8 @@ export function registerEmailTools(server: McpServer): void { + " Supports optional text search via query/queryScope.", inputSchema: z.object({ folder: z.string().default("Inbox").describe( - "Folder name (Inbox, Sent, Drafts, Deleted, Junk, or custom). Supports Russian folder names", + "Folder name (Inbox, Sent, Drafts, Deleted, Junk, or custom)." + + " Supports Russian folder names", ), limit: z.number().default(10).describe( "Maximum number of emails to return", @@ -297,7 +298,8 @@ export function registerEmailTools(server: McpServer): void { "download_attachments", { description: "Download all file attachments from an email to disk." - + " Inline (embedded) attachments are skipped — only standalone file attachments are downloaded.", + + " Inline (embedded) attachments are skipped —" + + " only standalone file attachments are downloaded.", inputSchema: z.object({ itemId: z.string().describe("The Exchange ItemId of the email"), targetFolder: z.string().default("/tmp/attachments").describe( diff --git a/tools/folders.ts b/tools/folders.ts index 36974b5..539284b 100644 --- a/tools/folders.ts +++ b/tools/folders.ts @@ -9,7 +9,8 @@ export function registerFolderTools(server: McpServer): void { description: "List mailbox folders from the Exchange mailbox.", inputSchema: z.object({ parentFolderId: z.string().default("msgfolderroot").describe( - "Parent folder to list children of. Supports distinguished names (e.g. inbox, calendar)", + "Parent folder to list children of." + + " Supports distinguished names (e.g. inbox, calendar)", ), recursive: z.boolean().default(false).describe( "If true, recursively traverse all subfolders", diff --git a/tools/people.ts b/tools/people.ts index b47ef7e..ffdd2eb 100644 --- a/tools/people.ts +++ b/tools/people.ts @@ -7,7 +7,8 @@ export function registerPeopleTools(server: McpServer): void { "find_person", { description: - "Search for people in the corporate directory (Active Directory) by name, email, or keyword.", + "Search for people in the corporate directory (Active Directory)" + + " by name, email, or keyword.", inputSchema: z.object({ query: z.string().describe( "Name, email address, or keyword to search for", -- 2.52.0 From 8dcdab97ce97cf9393803dec2725a8fd0b1ee364 Mon Sep 17 00:00:00 2001 From: albnnc Date: Fri, 10 Jul 2026 17:09:34 +0300 Subject: [PATCH 5/8] w --- types/calendar_event.ts | 17 ----------------- types/free_slot.ts | 6 ------ types/meeting_result.ts | 11 ----------- types/person.ts | 17 ----------------- 4 files changed, 51 deletions(-) delete mode 100644 types/calendar_event.ts delete mode 100644 types/free_slot.ts delete mode 100644 types/meeting_result.ts delete mode 100644 types/person.ts diff --git a/types/calendar_event.ts b/types/calendar_event.ts deleted file mode 100644 index 1e65734..0000000 --- a/types/calendar_event.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface CalendarEvent { - subject: string; - start: string; - end: string; - location: string; - isAllDay: boolean; - isCancelled: boolean; - isMeeting: boolean; - isRecurring: boolean; - organizer: string; - organizerEmail: string; - myResponse: string; - itemId: string; - body: string; - requiredAttendees: string[]; - optionalAttendees: string[]; -} diff --git a/types/free_slot.ts b/types/free_slot.ts deleted file mode 100644 index cae3873..0000000 --- a/types/free_slot.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface FreeSlot { - date: string; - start: string; - end: string; - durationMinutes: number; -} diff --git a/types/meeting_result.ts b/types/meeting_result.ts deleted file mode 100644 index fc8cbf7..0000000 --- a/types/meeting_result.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface MeetingResult { - success: boolean; - subject: string; - date: string; - startTime: string; - endTime: string; - location: string; - requiredAttendees: string[]; - optionalAttendees: string[]; - error: string; -} diff --git a/types/person.ts b/types/person.ts deleted file mode 100644 index 1916cc3..0000000 --- a/types/person.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Person { - name: string; - email: string; - mailboxType: string; - firstName: string; - lastName: string; - jobTitle: string; - department: string; - company: string; - office: string; - alias: string; - manager: string; - managerEmail: string; - phones: Record; - address: string; - directReports: Array<{ name: string; email: string }>; -} -- 2.52.0 From 2f0bfba1cfd7e5b3cf8f38322ca3f6ea88e79f2a Mon Sep 17 00:00:00 2001 From: albnnc Date: Fri, 10 Jul 2026 17:15:11 +0300 Subject: [PATCH 6/8] w --- ews_client.ts | 12 +----------- tools/calendar.ts | 4 ++-- tools/email.ts | 16 +++------------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/ews_client.ts b/ews_client.ts index fe90b3c..75fb0f1 100644 --- a/ews_client.ts +++ b/ews_client.ts @@ -377,7 +377,7 @@ ${restriction} return folders; } - extractEmailSummary(item: any): Email { + extractEmail(item: any): Email { const itemType = item["@_xsi_type"] ?? item.__type ?? ""; const isMeeting = /MeetingRequest|MeetingResponse|MeetingCancellation|CalendarItem/i.test( @@ -429,12 +429,6 @@ ${restriction} email.end = item.End ?? item.EndWallClock ?? ""; } - return email; - } - - extractEmailDetails(item: any): Email { - const email = this.extractEmailSummary(item); - const bodyVal = item.Body?.Value ?? item.Body?.["#text"] ?? ""; const bodyType = item.Body?.["@_BodyType"] ?? item.Body?.BodyType ?? "Text"; @@ -476,10 +470,6 @@ ${restriction} isInline: a.IsInline === "true" || a.IsInline === true, })); - const isMeeting = - /MeetingRequest|MeetingResponse|MeetingCancellation|CalendarItem/i.test( - item["@_xsi_type"] ?? "", - ); if (isMeeting) { email.location = item.Location ?? item.EnhancedLocation?.DisplayName ?? ""; diff --git a/tools/calendar.ts b/tools/calendar.ts index 00f18d8..aecd30c 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -73,7 +73,7 @@ export function registerCalendarTools(server: McpServer): void { }; if (includeBody && event.itemId) { - const details = client.extractEmailDetails(item); + const details = client.extractEmail(item); event.body = details.body; event.requiredAttendees = details.requiredAttendees ?? []; event.optionalAttendees = details.optionalAttendees ?? []; @@ -138,7 +138,7 @@ export function registerCalendarTools(server: McpServer): void { try { const client = new EwsClient(loadConfig()); const item = await client.getItem(itemId); - const email = client.extractEmailDetails(item); + const email = client.extractEmail(item); const attachments = email.attachments ?? []; const fileAttachments = attachments.filter( diff --git a/tools/email.ts b/tools/email.ts index 66a9981..4b455a6 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -139,17 +139,7 @@ export function registerEmailTools(server: McpServer): void { const emails = []; for (const item of items) { - const email = client.extractEmailSummary(item); - - if (includeBody && email.itemId) { - const details = client.extractEmailDetails(item); - email.to = details.to; - email.cc = details.cc; - email.body = details.body; - email.hasLinks = details.hasLinks; - } - - emails.push(email); + emails.push(client.extractEmail(item)); } return { @@ -205,7 +195,7 @@ export function registerEmailTools(server: McpServer): void { try { const client = getClient(); const item = await client.getItem(itemId); - const email = client.extractEmailDetails(item); + const email = client.extractEmail(item); return { content: [{ type: "text" as const, text: JSON.stringify(email) }], @@ -326,7 +316,7 @@ export function registerEmailTools(server: McpServer): void { try { const client = new EwsClient(loadConfig()); const item = await client.getItem(itemId); - const email = client.extractEmailDetails(item); + const email = client.extractEmail(item); const attachments = email.attachments ?? []; const fileAttachments = attachments.filter( -- 2.52.0 From 07db914ca0b55563c1eced68c7753daaf1031ae9 Mon Sep 17 00:00:00 2001 From: albnnc Date: Fri, 10 Jul 2026 20:17:43 +0300 Subject: [PATCH 7/8] w --- ews_client.ts => client/ews_client.ts | 300 ++++---------------------- main.ts | 2 +- tools/auth.ts | 4 +- tools/availability.ts | 2 +- tools/calendar.ts | 2 +- tools/email.ts | 2 +- tools/folders.ts | 2 +- tools/people.ts | 2 +- types/email.ts | 32 --- types/folder.ts | 7 - types/login_config.ts | 6 - utils/config.ts | 59 +++++ utils/extract_email.ts | 156 ++++++++++++++ utils/extract_folder.ts | 101 +++++++++ utils/html_to_text.ts | 20 ++ 15 files changed, 382 insertions(+), 315 deletions(-) rename ews_client.ts => client/ews_client.ts (59%) delete mode 100644 types/email.ts delete mode 100644 types/folder.ts delete mode 100644 types/login_config.ts create mode 100644 utils/config.ts create mode 100644 utils/extract_email.ts create mode 100644 utils/extract_folder.ts create mode 100644 utils/html_to_text.ts diff --git a/ews_client.ts b/client/ews_client.ts similarity index 59% rename from ews_client.ts rename to client/ews_client.ts index 75fb0f1..d3f96ae 100644 --- a/ews_client.ts +++ b/client/ews_client.ts @@ -1,36 +1,19 @@ import { XMLParser } from "fast-xml-parser"; -import fs from "node:fs"; import https from "node:https"; import { createRequire } from "node:module"; -import path from "node:path"; import { promisify } from "node:util"; -import type { Email } from "./types/email.ts"; -import type { Folder } from "./types/folder.ts"; -import type { LoginConfig } from "./types/login_config.ts"; - -let inMemoryPassword: string | null = null; -let dataDir: string = "./data"; - -export function initDataDir(dir: string): void { - dataDir = path.resolve(dir); - fs.mkdirSync(dataDir, { recursive: true }); -} - -export function setPassword(password: string): void { - inMemoryPassword = password; -} - -export function getPassword(): string | null { - return inMemoryPassword; -} - -export function hasPassword(): boolean { - return inMemoryPassword !== null; -} - -export function clearPassword(): void { - inMemoryPassword = null; -} +import { extractEmail, type Email } from "../utils/extract_email.ts"; +import { extractFolders, type Folder } from "../utils/extract_folder.ts"; +import { + loadConfig, + saveConfig, + clearConfig, + setPassword, + getPassword, + hasPassword, + initDataDir, + type LoginConfig, +} from "../utils/config.ts"; const ntlm: any = createRequire(import.meta.url)("httpntlm"); const postAsync = promisify(ntlm.post.bind(ntlm)); @@ -43,13 +26,6 @@ const AGENT = new https.Agent({ secureOptions: SSL_OP_LEGACY_SERVER_CONNECT, }); -const PARSER = new XMLParser({ - ignoreAttributes: false, - attributeNamePrefix: "@_", - removeNSPrefix: true, - textNodeName: "#text", -}); - const DISTINGUISHED_FOLDERS: Record = { inbox: "inbox", "входящие": "inbox", @@ -78,30 +54,12 @@ ${body} `; } -function configFilePath(): string { - return path.join(dataDir, "config.json"); -} - -export function loadConfig(): LoginConfig { - const filePath = configFilePath(); - if (!fs.existsSync(filePath)) { - throw new Error("Not logged in. Use the login tool first."); - } - return JSON.parse(fs.readFileSync(filePath, "utf-8")) as LoginConfig; -} - -export function saveConfig(config: LoginConfig): void { - const filePath = configFilePath(); - fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8"); -} - -export function clearConfig(): void { - const filePath = configFilePath(); - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - clearPassword(); -} +const PARSER = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: "@_", + removeNSPrefix: true, + textNodeName: "#text", +}); export class EwsClient { private config: LoginConfig; @@ -110,10 +68,7 @@ export class EwsClient { constructor(config: LoginConfig, password?: string) { this.config = config; this.ewsUrl = `${config.serverUrl.replace(/\/+$/, "")}/EWS/Exchange.asmx`; - - if (password) { - setPassword(password); - } + if (password) setPassword(password); } private get domain(): string { @@ -144,8 +99,7 @@ export class EwsClient { throw new Error(`NTLM request failed, status=${res.statusCode}`); } - const parsed = PARSER.parse(res.body); - return parsed; + return PARSER.parse(res.body); } private extractResponseMessages(data: any): any[] { @@ -176,7 +130,7 @@ export class EwsClient { async getFolderId(folderName: string): Promise { const lower = folderName.toLowerCase(); - const distinguished = DISTINGUISHED_FOLDERS[lower]; + const distinguished = (DISTINGUISHED_FOLDERS as Record)[lower]; if (distinguished) { const soap = buildSoapEnvelope(`\ @@ -251,7 +205,7 @@ export class EwsClient { } = options; const isDistinguished = - DISTINGUISHED_FOLDERS[folderId.toLowerCase()] !== undefined; + (DISTINGUISHED_FOLDERS as Record)[folderId.toLowerCase()] !== undefined; const folderIdXml = isDistinguished ? `` @@ -331,198 +285,17 @@ ${restriction} parentFolderId: string, recursive: boolean = false, ): Promise { - const traversal = recursive ? "Deep" : "Shallow"; - const isDistinguished = - DISTINGUISHED_FOLDERS[parentFolderId.toLowerCase()] !== undefined - || ["msgfolderroot"].includes(parentFolderId.toLowerCase()); - - const folderIdXml = isDistinguished - ? `` - : ``; - - const soap = buildSoapEnvelope(`\ - - - Default - - - ${folderIdXml} - - - `); - - const data = await this.soapRequest( - soap, - "http://schemas.microsoft.com/exchange/services/2006/messages/FindFolder", + return extractFolders( + (body, soapAction) => this.soapRequest(body, soapAction), + parentFolderId, + recursive, ); - - const folders: Folder[] = []; - - for (const msg of this.extractResponseMessages(data)) { - const folderList = msg?.RootFolder?.Folders?.Folder; - if (!folderList) continue; - const list = Array.isArray(folderList) ? folderList : [folderList]; - - for (const f of list) { - folders.push({ - name: f.DisplayName ?? "Unknown", - id: f.FolderId?.["@_Id"] ?? "", - totalCount: f.TotalCount ?? 0, - unreadCount: f.UnreadCount ?? 0, - childFolderCount: f.ChildFolderCount ?? 0, - }); - } - } - - return folders; } extractEmail(item: any): Email { - const itemType = item["@_xsi_type"] ?? item.__type ?? ""; - const isMeeting = - /MeetingRequest|MeetingResponse|MeetingCancellation|CalendarItem/i.test( - itemType, - ); - - const fromMailbox = item.From?.Mailbox - ?? item.Organizer?.Mailbox - ?? item.Sender?.Mailbox - ?? {}; - - const email: Email = { - subject: item.Subject ?? "(No subject)", - from: fromMailbox.EmailAddress ?? "", - fromName: fromMailbox.Name ?? "", - date: item.DateTimeSent ?? item.DateTimeReceived ?? item.DateTimeCreated - ?? "", - isRead: item.IsRead === "true" || item.IsRead === true, - hasAttachments: item.HasAttachments === "true" - || item.HasAttachments === true, - hasLinks: false, - itemId: item.ItemId?.["@_Id"] ?? "", - size: item.Size ? Number(item.Size) : 0, - isMeeting, - itemType: itemType || "Message", - to: [], - cc: [], - body: "", - bodyType: "Text", - attachments: [], - preview: item.Preview ?? "", - }; - - if (item.DisplayTo) { - email.to = item.DisplayTo.split(";").map((t: string) => t.trim()).filter( - Boolean, - ); - } - if (item.DisplayCc) { - email.cc = item.DisplayCc.split(";").map((c: string) => c.trim()).filter( - Boolean, - ); - } - - if (isMeeting) { - email.location = item.Location ?? ""; - email.start = item.Start ?? item.StartWallClock ?? item.ReminderDueBy - ?? ""; - email.end = item.End ?? item.EndWallClock ?? ""; - } - - const bodyVal = item.Body?.Value ?? item.Body?.["#text"] ?? ""; - const bodyType = item.Body?.["@_BodyType"] ?? item.Body?.BodyType ?? "Text"; - - if (bodyType === "HTML") { - email.hasLinks = / { - const name = r.Name ?? ""; - const addr = r.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - - const ccRecipients = item.CcRecipients?.Mailbox ?? []; - email.cc = (Array.isArray(ccRecipients) ? ccRecipients : [ccRecipients]) - .map((r: any) => { - const name = r.Name ?? ""; - const addr = r.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - - const attachments = item.Attachments?.FileAttachment ?? []; - const attList = Array.isArray(attachments) ? attachments : [attachments]; - email.attachments = attList - .filter((a: any) => a) - .map((a: any) => ({ - name: a.Name ?? "", - size: a.Size ? Number(a.Size) : 0, - contentType: a.ContentType ?? "", - attachmentId: a.AttachmentId?.["@_Id"] ?? "", - isInline: a.IsInline === "true" || a.IsInline === true, - })); - - if (isMeeting) { - email.location = item.Location ?? item.EnhancedLocation?.DisplayName - ?? ""; - email.start = item.Start ?? ""; - email.end = item.End ?? ""; - - const reqAtt = item.RequiredAttendees?.Attendee ?? []; - email.requiredAttendees = (Array.isArray(reqAtt) ? reqAtt : [reqAtt]) - .map((a: any) => { - const mb = a.Mailbox ?? {}; - const name = mb.Name ?? ""; - const addr = mb.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - - const optAtt = item.OptionalAttendees?.Attendee ?? []; - email.optionalAttendees = (Array.isArray(optAtt) ? optAtt : [optAtt]) - .map((a: any) => { - const mb = a.Mailbox ?? {}; - const name = mb.Name ?? ""; - const addr = mb.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - } - - return email; + return extractEmail(item); } - private htmlToText(html: string): string { - if (!html) return ""; - let text = html.replace(/]*>.*?<\/script>/gis, ""); - text = text.replace(/]*>.*?<\/style>/gis, ""); - text = text.replace(//gi, "\n"); - text = text.replace(/]*>/gi, "\n"); - text = text.replace(/<\/p>/gi, ""); - text = text.replace(/]*>/gi, "\n"); - text = text.replace(/<\/div>/gi, ""); - text = text.replace(/<[^>]+>/g, ""); - text = text.replace(/ /g, " "); - text = text.replace(/&/g, "&"); - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); - text = text.replace(/"/g, "\""); - text = text.replace(/'/g, "'"); - text = text.replace(/\n\s*\n/g, "\n\n"); - text = text.replace(/[ \t]+/g, " "); - return text.trim(); - } - - // ── UpdateItem (for marking read/unread, etc.) ────────────────────────── - async updateItems( itemIds: string[], updates: { fieldUri: string; value: string | boolean }[], @@ -572,8 +345,6 @@ ${changesXml} return this.extractResponseMessages(data); } - // ── GetAttachment ─────────────────────────────────────────────────────── - async getAttachment( attachmentId: string, ): Promise<{ content: Buffer; name: string; contentType: string }> { @@ -607,8 +378,6 @@ ${changesXml} throw new Error(`Attachment '${attachmentId}' not found`); } - // ── FindItem with CalendarView ────────────────────────────────────────── - async findCalendarItems( folderId: string, startDate: string, @@ -640,8 +409,6 @@ ${changesXml} return []; } - // ── GetUserAvailability ───────────────────────────────────────────────── - async getUserAvailability( emails: string[], startDate: string, @@ -689,8 +456,6 @@ ${mailboxDataXml} return data; } - // ── ResolveNames (directory search) ───────────────────────────────────── - async resolveNames( query: string, fullContact: boolean = true, @@ -717,3 +482,14 @@ ${mailboxDataXml} return []; } } + +// Re-export for backward compatibility +export { + initDataDir, + loadConfig, + saveConfig, + clearConfig, + setPassword, + getPassword, + hasPassword, +} from "../utils/config.ts"; diff --git a/main.ts b/main.ts index 3ac4048..7f54e75 100644 --- a/main.ts +++ b/main.ts @@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { Command } from "commander"; import crypto from "node:crypto"; import http from "node:http"; -import { initDataDir } from "./ews_client.ts"; +import { initDataDir } from "./client/ews_client.ts"; import { registerAuthTools } from "./tools/auth.ts"; import { registerAvailabilityTools } from "./tools/availability.ts"; import { registerCalendarTools } from "./tools/calendar.ts"; diff --git a/tools/auth.ts b/tools/auth.ts index a8c6308..b92abd8 100644 --- a/tools/auth.ts +++ b/tools/auth.ts @@ -7,8 +7,8 @@ import { loadConfig, saveConfig, setPassword, -} from "../ews_client.ts"; -import type { LoginConfig } from "../types/login_config.ts"; +} from "../client/ews_client.ts"; +import { type LoginConfig } from "../utils/config.ts"; export function registerAuthTools(server: McpServer): void { server.registerTool( diff --git a/tools/availability.ts b/tools/availability.ts index 2cd8a91..a164a16 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -1,6 +1,6 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerAvailabilityTools(server: McpServer): void { server.registerTool( diff --git a/tools/calendar.ts b/tools/calendar.ts index aecd30c..b881534 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -2,7 +2,7 @@ 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"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerCalendarTools(server: McpServer): void { server.registerTool( diff --git a/tools/email.ts b/tools/email.ts index 4b455a6..7926d60 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -2,7 +2,7 @@ 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"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; function getClient(): EwsClient { return new EwsClient(loadConfig()); diff --git a/tools/folders.ts b/tools/folders.ts index 539284b..103c425 100644 --- a/tools/folders.ts +++ b/tools/folders.ts @@ -1,6 +1,6 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerFolderTools(server: McpServer): void { server.registerTool( diff --git a/tools/people.ts b/tools/people.ts index ffdd2eb..946d632 100644 --- a/tools/people.ts +++ b/tools/people.ts @@ -1,6 +1,6 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerPeopleTools(server: McpServer): void { server.registerTool( diff --git a/types/email.ts b/types/email.ts deleted file mode 100644 index 1ac1b24..0000000 --- a/types/email.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface Email { - subject: string; - from: string; - fromName: string; - date: string; - isRead: boolean; - hasAttachments: boolean; - hasLinks: boolean; - itemId: string; - size: number; - isMeeting: boolean; - itemType: string; - to: string[]; - cc: string[]; - body: string; - bodyType: string; - attachments: Attachment[]; - preview: string; - location?: string; - start?: string; - end?: string; - requiredAttendees?: string[]; - optionalAttendees?: string[]; -} - -export interface Attachment { - name: string; - size: number; - contentType: string; - attachmentId: string; - isInline: boolean; -} diff --git a/types/folder.ts b/types/folder.ts deleted file mode 100644 index 6bef6e0..0000000 --- a/types/folder.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Folder { - name: string; - id: string; - totalCount: number; - unreadCount: number; - childFolderCount: number; -} diff --git a/types/login_config.ts b/types/login_config.ts deleted file mode 100644 index ef2c777..0000000 --- a/types/login_config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface LoginConfig { - serverUrl: string; - email: string; - username: string; - domain?: string; -} diff --git a/utils/config.ts b/utils/config.ts new file mode 100644 index 0000000..62dd440 --- /dev/null +++ b/utils/config.ts @@ -0,0 +1,59 @@ +export interface LoginConfig { + serverUrl: string; + email: string; + username: string; + domain?: string; +} + +import fs from "node:fs"; +import path from "node:path"; + +let dataDir: string = "./data"; + +export function initDataDir(dir: string): void { + dataDir = path.resolve(dir); + fs.mkdirSync(dataDir, { recursive: true }); +} + +let inMemoryPassword: string | null = null; + +export function setPassword(password: string): void { + inMemoryPassword = password; +} + +export function getPassword(): string | null { + return inMemoryPassword; +} + +export function hasPassword(): boolean { + return inMemoryPassword !== null; +} + +export function clearPassword(): void { + inMemoryPassword = null; +} + +function configFilePath(): string { + return path.join(dataDir, "config.json"); +} + +export function loadConfig(): LoginConfig { + const filePath = configFilePath(); + if (!fs.existsSync(filePath)) { + throw new Error("Not logged in. Use the login tool first."); + } + return JSON.parse(fs.readFileSync(filePath, "utf-8")) as LoginConfig; +} + +export function saveConfig(config: LoginConfig): void { + const filePath = configFilePath(); + fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8"); +} + +export function clearConfig(): void { + const filePath = configFilePath(); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + clearPassword(); +} diff --git a/utils/extract_email.ts b/utils/extract_email.ts new file mode 100644 index 0000000..45572f3 --- /dev/null +++ b/utils/extract_email.ts @@ -0,0 +1,156 @@ +import { htmlToText } from "./html_to_text.ts"; + +export interface Attachment { + name: string; + size: number; + contentType: string; + attachmentId: string; + isInline: boolean; +} + +export interface Email { + subject: string; + from: string; + fromName: string; + date: string; + isRead: boolean; + hasAttachments: boolean; + hasLinks: boolean; + itemId: string; + size: number; + isMeeting: boolean; + itemType: string; + to: string[]; + cc: string[]; + body: string; + bodyType: string; + attachments: Attachment[]; + preview: string; + location?: string; + start?: string; + end?: string; + requiredAttendees?: string[]; + optionalAttendees?: string[]; +} + +export function extractEmail(item: any): Email { + const itemType = item["@_xsi_type"] ?? item.__type ?? ""; + const isMeeting = + /MeetingRequest|MeetingResponse|MeetingCancellation|CalendarItem/i.test( + itemType, + ); + + const fromMailbox = item.From?.Mailbox + ?? item.Organizer?.Mailbox + ?? item.Sender?.Mailbox + ?? {}; + + const email: Email = { + subject: item.Subject ?? "(No subject)", + from: fromMailbox.EmailAddress ?? "", + fromName: fromMailbox.Name ?? "", + date: item.DateTimeSent ?? item.DateTimeReceived ?? item.DateTimeCreated + ?? "", + isRead: item.IsRead === "true" || item.IsRead === true, + hasAttachments: item.HasAttachments === "true" + || item.HasAttachments === true, + hasLinks: false, + itemId: item.ItemId?.["@_Id"] ?? "", + size: item.Size ? Number(item.Size) : 0, + isMeeting, + itemType: itemType || "Message", + to: [], + cc: [], + body: "", + bodyType: "Text", + attachments: [], + preview: item.Preview ?? "", + }; + + if (item.DisplayTo) { + email.to = item.DisplayTo.split(";").map((t: string) => t.trim()).filter( + Boolean, + ); + } + if (item.DisplayCc) { + email.cc = item.DisplayCc.split(";").map((c: string) => c.trim()).filter( + Boolean, + ); + } + + if (isMeeting) { + email.location = item.Location ?? ""; + email.start = item.Start ?? item.StartWallClock ?? item.ReminderDueBy + ?? ""; + email.end = item.End ?? item.EndWallClock ?? ""; + } + + const bodyVal = item.Body?.Value ?? item.Body?.["#text"] ?? ""; + const bodyType = item.Body?.["@_BodyType"] ?? item.Body?.BodyType ?? "Text"; + + if (bodyType === "HTML") { + email.hasLinks = / { + const name = r.Name ?? ""; + const addr = r.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + + const ccRecipients = item.CcRecipients?.Mailbox ?? []; + email.cc = (Array.isArray(ccRecipients) ? ccRecipients : [ccRecipients]) + .map((r: any) => { + const name = r.Name ?? ""; + const addr = r.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + + const attachments = item.Attachments?.FileAttachment ?? []; + const attList = Array.isArray(attachments) ? attachments : [attachments]; + email.attachments = attList + .filter((a: any) => a) + .map((a: any) => ({ + name: a.Name ?? "", + size: a.Size ? Number(a.Size) : 0, + contentType: a.ContentType ?? "", + attachmentId: a.AttachmentId?.["@_Id"] ?? "", + isInline: a.IsInline === "true" || a.IsInline === true, + })); + + if (isMeeting) { + email.location = item.Location ?? item.EnhancedLocation?.DisplayName ?? ""; + email.start = item.Start ?? ""; + email.end = item.End ?? ""; + + const reqAtt = item.RequiredAttendees?.Attendee ?? []; + email.requiredAttendees = (Array.isArray(reqAtt) ? reqAtt : [reqAtt]) + .map((a: any) => { + const mb = a.Mailbox ?? {}; + const name = mb.Name ?? ""; + const addr = mb.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + + const optAtt = item.OptionalAttendees?.Attendee ?? []; + email.optionalAttendees = (Array.isArray(optAtt) ? optAtt : [optAtt]) + .map((a: any) => { + const mb = a.Mailbox ?? {}; + const name = mb.Name ?? ""; + const addr = mb.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + } + + return email; +} diff --git a/utils/extract_folder.ts b/utils/extract_folder.ts new file mode 100644 index 0000000..464e428 --- /dev/null +++ b/utils/extract_folder.ts @@ -0,0 +1,101 @@ +export interface Folder { + name: string; + id: string; + totalCount: number; + unreadCount: number; + childFolderCount: number; +} + +type SoapRequest = (body: string, soapAction: string) => Promise; + +export async function extractFolders( + soapRequest: SoapRequest, + parentFolderId: string, + recursive: boolean = false, +): Promise { + const DISTINGUISHED_FOLDERS: Record = { + inbox: "inbox", + "входящие": "inbox", + sent: "sentitems", + "отправленные": "sentitems", + drafts: "drafts", + "черновики": "drafts", + deleted: "deleteditems", + "удаленные": "deleteditems", + junk: "junkemail", + "нежелательная почта": "junkemail", + outbox: "outbox", + "исходящие": "outbox", + calendar: "calendar", + "календарь": "calendar", + }; + + function buildSoapEnvelope(body: string): string { + return ` + + +${body} + +`; + } + + function extractResponseMessages(data: any): any[] { + const body = data?.Envelope?.Body; + if (!body) return []; + const firstKey = Object.keys(body).find((k) => k.endsWith("Response")); + if (!firstKey) return []; + const rm = body[firstKey]?.ResponseMessages; + if (!rm) return []; + const msgKey = Object.keys(rm).find((k) => k.endsWith("ResponseMessage")); + if (!msgKey) return []; + const msgs = rm[msgKey]; + return Array.isArray(msgs) ? msgs : [msgs]; + } + + const traversal = recursive ? "Deep" : "Shallow"; + const isDistinguished = + DISTINGUISHED_FOLDERS[parentFolderId.toLowerCase()] !== undefined + || ["msgfolderroot"].includes(parentFolderId.toLowerCase()); + + const folderIdXml = isDistinguished + ? `` + : ``; + + const soap = buildSoapEnvelope(`\ + + + Default + + + ${folderIdXml} + + + `); + + const data = await soapRequest( + soap, + "http://schemas.microsoft.com/exchange/services/2006/messages/FindFolder", + ); + + const folders: Folder[] = []; + + for (const msg of extractResponseMessages(data)) { + const folderList = msg?.RootFolder?.Folders?.Folder; + if (!folderList) continue; + const list = Array.isArray(folderList) ? folderList : [folderList]; + + for (const f of list) { + folders.push({ + name: f.DisplayName ?? "Unknown", + id: f.FolderId?.["@_Id"] ?? "", + totalCount: f.TotalCount ?? 0, + unreadCount: f.UnreadCount ?? 0, + childFolderCount: f.ChildFolderCount ?? 0, + }); + } + } + + return folders; +} diff --git a/utils/html_to_text.ts b/utils/html_to_text.ts new file mode 100644 index 0000000..579520b --- /dev/null +++ b/utils/html_to_text.ts @@ -0,0 +1,20 @@ +export function htmlToText(html: string): string { + if (!html) return ""; + let text = html.replace(/]*>.*?<\/script>/gis, ""); + text = text.replace(/]*>.*?<\/style>/gis, ""); + text = text.replace(//gi, "\n"); + text = text.replace(/]*>/gi, "\n"); + text = text.replace(/<\/p>/gi, ""); + text = text.replace(/]*>/gi, "\n"); + text = text.replace(/<\/div>/gi, ""); + text = text.replace(/<[^>]+>/g, ""); + text = text.replace(/ /g, " "); + text = text.replace(/&/g, "&"); + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + text = text.replace(/"/g, "\""); + text = text.replace(/'/g, "'"); + text = text.replace(/\n\s*\n/g, "\n\n"); + text = text.replace(/[ \t]+/g, " "); + return text.trim(); +} -- 2.52.0 From a4741b68209c4739370262e1d3910368cdefb62e Mon Sep 17 00:00:00 2001 From: albnnc Date: Sat, 11 Jul 2026 11:52:15 +0300 Subject: [PATCH 8/8] w --- client/ews_client.ts | 15 ++------------- main.ts | 2 +- tools/auth.ts | 12 +++--------- tools/availability.ts | 3 ++- tools/calendar.ts | 3 ++- tools/email.ts | 3 ++- tools/folders.ts | 3 ++- tools/people.ts | 3 ++- utils/data.ts | 13 +++++++++++++ utils/{config.ts => login.ts} | 20 ++++++-------------- 10 files changed, 35 insertions(+), 42 deletions(-) create mode 100644 utils/data.ts rename utils/{config.ts => login.ts} (79%) diff --git a/client/ews_client.ts b/client/ews_client.ts index d3f96ae..e55b33f 100644 --- a/client/ews_client.ts +++ b/client/ews_client.ts @@ -4,6 +4,7 @@ import { createRequire } from "node:module"; import { promisify } from "node:util"; import { extractEmail, type Email } from "../utils/extract_email.ts"; import { extractFolders, type Folder } from "../utils/extract_folder.ts"; +import { initDataDir } from "../utils/data.ts"; import { loadConfig, saveConfig, @@ -11,9 +12,8 @@ import { setPassword, getPassword, hasPassword, - initDataDir, type LoginConfig, -} from "../utils/config.ts"; +} from "../utils/login.ts"; const ntlm: any = createRequire(import.meta.url)("httpntlm"); const postAsync = promisify(ntlm.post.bind(ntlm)); @@ -482,14 +482,3 @@ ${mailboxDataXml} return []; } } - -// Re-export for backward compatibility -export { - initDataDir, - loadConfig, - saveConfig, - clearConfig, - setPassword, - getPassword, - hasPassword, -} from "../utils/config.ts"; diff --git a/main.ts b/main.ts index 7f54e75..1779f91 100644 --- a/main.ts +++ b/main.ts @@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { Command } from "commander"; import crypto from "node:crypto"; import http from "node:http"; -import { initDataDir } from "./client/ews_client.ts"; +import { initDataDir } from "./utils/data.ts"; import { registerAuthTools } from "./tools/auth.ts"; import { registerAvailabilityTools } from "./tools/availability.ts"; import { registerCalendarTools } from "./tools/calendar.ts"; diff --git a/tools/auth.ts b/tools/auth.ts index b92abd8..39d3cd4 100644 --- a/tools/auth.ts +++ b/tools/auth.ts @@ -1,14 +1,8 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { - clearConfig, - EwsClient, - hasPassword, - loadConfig, - saveConfig, - setPassword, -} from "../client/ews_client.ts"; -import { type LoginConfig } from "../utils/config.ts"; +import { clearConfig, hasPassword, loadConfig, saveConfig, setPassword } from "../utils/login.ts"; +import { EwsClient } from "../client/ews_client.ts"; +import { type LoginConfig } from "../utils/login.ts"; export function registerAuthTools(server: McpServer): void { server.registerTool( diff --git a/tools/availability.ts b/tools/availability.ts index a164a16..ce8570a 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -1,6 +1,7 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../client/ews_client.ts"; +import { loadConfig } from "../utils/login.ts"; +import { EwsClient } from "../client/ews_client.ts"; export function registerAvailabilityTools(server: McpServer): void { server.registerTool( diff --git a/tools/calendar.ts b/tools/calendar.ts index b881534..1c4023d 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -2,7 +2,8 @@ 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 "../client/ews_client.ts"; +import { loadConfig } from "../utils/login.ts"; +import { EwsClient } from "../client/ews_client.ts"; export function registerCalendarTools(server: McpServer): void { server.registerTool( diff --git a/tools/email.ts b/tools/email.ts index 7926d60..657833d 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -2,7 +2,8 @@ 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 "../client/ews_client.ts"; +import { loadConfig } from "../utils/login.ts"; +import { EwsClient } from "../client/ews_client.ts"; function getClient(): EwsClient { return new EwsClient(loadConfig()); diff --git a/tools/folders.ts b/tools/folders.ts index 103c425..7acdc64 100644 --- a/tools/folders.ts +++ b/tools/folders.ts @@ -1,6 +1,7 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../client/ews_client.ts"; +import { loadConfig } from "../utils/login.ts"; +import { EwsClient } from "../client/ews_client.ts"; export function registerFolderTools(server: McpServer): void { server.registerTool( diff --git a/tools/people.ts b/tools/people.ts index 946d632..db9635b 100644 --- a/tools/people.ts +++ b/tools/people.ts @@ -1,6 +1,7 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../client/ews_client.ts"; +import { loadConfig } from "../utils/login.ts"; +import { EwsClient } from "../client/ews_client.ts"; export function registerPeopleTools(server: McpServer): void { server.registerTool( diff --git a/utils/data.ts b/utils/data.ts new file mode 100644 index 0000000..98bc022 --- /dev/null +++ b/utils/data.ts @@ -0,0 +1,13 @@ +import fs from "node:fs"; +import path from "node:path"; + +let dataDir: string = "./data"; + +export function initDataDir(dir: string): void { + dataDir = path.resolve(dir); + fs.mkdirSync(dataDir, { recursive: true }); +} + +export function configFilePath(): string { + return path.join(dataDir, "config.json"); +} diff --git a/utils/config.ts b/utils/login.ts similarity index 79% rename from utils/config.ts rename to utils/login.ts index 62dd440..2a9ea45 100644 --- a/utils/config.ts +++ b/utils/login.ts @@ -1,3 +1,9 @@ +import fs from "node:fs"; +import { configFilePath, initDataDir } from "./data.ts"; + +// Re-export for backward compatibility +export { initDataDir }; + export interface LoginConfig { serverUrl: string; email: string; @@ -5,16 +11,6 @@ export interface LoginConfig { domain?: string; } -import fs from "node:fs"; -import path from "node:path"; - -let dataDir: string = "./data"; - -export function initDataDir(dir: string): void { - dataDir = path.resolve(dir); - fs.mkdirSync(dataDir, { recursive: true }); -} - let inMemoryPassword: string | null = null; export function setPassword(password: string): void { @@ -33,10 +29,6 @@ export function clearPassword(): void { inMemoryPassword = null; } -function configFilePath(): string { - return path.join(dataDir, "config.json"); -} - export function loadConfig(): LoginConfig { const filePath = configFilePath(); if (!fs.existsSync(filePath)) { -- 2.52.0