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 {