refactor: drop skill file, rework client #3

Merged
albnnc merged 8 commits from able-swan into main 2026-07-11 08:52:56 +00:00
6 changed files with 268 additions and 72 deletions
Showing only changes of commit 979e974521 - Show all commits
+37 -5
View File
@@ -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.",
},
};
},
);
+47 -13
View File
@@ -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 {
+51 -5
View File
@@ -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 {
+89 -35
View File
@@ -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;
@@ -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 {
+13 -4
View File
@@ -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 {
+23 -2
View File
@@ -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 {