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",