import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; import { EwsClient, loadConfig } from "../ews_client.ts"; function getClient(): EwsClient { return new EwsClient(loadConfig()); } export function registerEmailTools(server: McpServer): void { server.registerTool( "get_emails", { description: "Get emails from a mailbox folder", inputSchema: z.object({ folder: z.string().default("Inbox").describe( "Folder name (Inbox, Sent, Drafts, Deleted, Junk, or custom)", ), limit: z.number().default(10).describe( "Maximum number of emails to return (default 10, max 50)", ), offset: z.number().default(0).describe( "Number of emails to skip for pagination", ), includeBody: z.boolean().default(false).describe( "If True, fetch full body for each email (slower)", ), unreadOnly: z.boolean().default(false).describe( "If True, only return unread emails", ), idsOnly: z.boolean().default(false).describe( "If True, return only item IDs and dates (max limit 500)", ), }), }, async ({ folder, limit, offset, includeBody, unreadOnly, idsOnly }) => { try { const client = getClient(); const maxLimit = idsOnly ? 500 : 50; if (limit > maxLimit) limit = maxLimit; const folderId = await client.getFolderId(folder); const baseShape = idsOnly ? "IdOnly" : "AllProperties"; let restriction = ""; if (unreadOnly) { restriction = `\ `; } const items = await client.findItems(folderId, { limit, offset, baseShape, 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); 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); } return { content: [{ type: "text" as const, text: JSON.stringify({ emails, count: emails.length }), }], }; } catch (error: any) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: error.message ?? String(error) }), }], }; } }, ); server.registerTool( "get_email", { description: "Get a single email with full body and details", inputSchema: z.object({ itemId: z.string().describe( "The Exchange ItemId of the email to retrieve", ), }), }, async ({ itemId }) => { try { const client = getClient(); const item = await client.getItem(itemId); const email = client.extractEmailDetails(item); return { content: [{ type: "text" as const, text: JSON.stringify(email) }], }; } catch (error: any) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: error.message ?? String(error) }), }], }; } }, ); server.registerTool( "search_emails", { description: "Search emails by text across one or all folders", inputSchema: z.object({ query: z.string().describe("The text to search for"), folderId: z.string().optional().describe( "Optional folder ID to limit the search. When omitted, searches all mail folders", ), maxResults: z.number().default(20).describe( "Maximum number of results (default 20, max 100)", ), searchScope: z.enum(["all", "subject", "body", "from"]).default("all") .describe("Where to search"), }), }, async ({ query, folderId, maxResults, searchScope }) => { try { const client = getClient(); if (!query.trim()) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: "query must not be empty", results: [], }), }], }; } maxResults = Math.max(1, Math.min(maxResults, 100)); const fieldUriMap: Record = { subject: "item:Subject", body: "item:Body", from: "message:From", }; function containsExpression(fieldUri: string, value: string): string { return `\ /g, ">", ).replace(/"/g, """) }"/> `; } let restriction: string; if (searchScope === "all") { restriction = `\ ${containsExpression("item:Subject", query)} ${containsExpression("item:Body", query)} `; } else { const fieldUri = fieldUriMap[searchScope]; if (!fieldUri) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: `unsupported search_scope: ${searchScope}`, results: [], }), }], }; } restriction = `\ ${containsExpression(fieldUri, query)} `; } const traversal = folderId ? "Shallow" : "Deep"; if (folderId) { const items = await client.findItems(folderId, { limit: maxResults, restriction, traversal, }); const results = formatSearchResults(items, client, maxResults); return { content: [{ type: "text" as const, text: JSON.stringify({ query, searchScope, folderId, totalResults: results.length, results, }), }], }; } else { const folders = await client.findFolders("msgfolderroot", true); const allResults = []; for (const f of folders) { if (allResults.length >= maxResults) break; const remaining = maxResults - allResults.length; const items = await client.findItems(f.id, { limit: remaining, restriction, traversal: "Shallow", }); const formatted = formatSearchResults(items, client, remaining); for (const r of formatted) { r.folderId = f.id; r.folderName = f.name; allResults.push(r); if (allResults.length >= maxResults) break; } } return { content: [{ type: "text" as const, text: JSON.stringify({ query, searchScope, folderId: "all", totalResults: allResults.length, results: allResults, }), }], }; } } catch (error: any) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: error.message ?? String(error) }), }], }; } }, ); } function formatSearchResults( items: any[], client: EwsClient, maxResults: number, ): any[] { const results: any[] = []; for (const item of items) { if (results.length >= maxResults) break; const summary = client.extractEmailSummary(item); const bodyHtml = item.Body?.Value ?? item.Body?.["#text"] ?? ""; const bodyType = item.Body?.["@_BodyType"] ?? "HTML"; let bodyPreview = ""; if (bodyType === "HTML" && bodyHtml) { bodyPreview = bodyHtml.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ") .trim().slice(0, 200); } else { bodyPreview = bodyHtml.slice(0, 200); } summary.preview = bodyPreview; results.push(summary); } return results; }