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 {