ms365-mcp-server 1.1.9 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/dist/index.js +141 -7
- package/dist/utils/ms365-operations.js +311 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -308,6 +308,95 @@ Files are:
|
|
|
308
308
|
### Cleanup
|
|
309
309
|
Attachments are automatically cleaned up after 24 hours. You can also manually delete files from the `public/attachments` directory.
|
|
310
310
|
|
|
311
|
+
### Enhanced Draft Email Operations
|
|
312
|
+
```javascript
|
|
313
|
+
// Create a basic draft
|
|
314
|
+
{
|
|
315
|
+
"tool": "manage_email",
|
|
316
|
+
"arguments": {
|
|
317
|
+
"action": "draft",
|
|
318
|
+
"draftTo": ["colleague@company.com"],
|
|
319
|
+
"draftSubject": "Project Update",
|
|
320
|
+
"draftBody": "Draft content here...",
|
|
321
|
+
"draftBodyType": "text"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Create a threaded reply draft (automatically appears in email thread)
|
|
326
|
+
{
|
|
327
|
+
"tool": "manage_email",
|
|
328
|
+
"arguments": {
|
|
329
|
+
"action": "reply_draft",
|
|
330
|
+
"originalMessageId": "original_email_id",
|
|
331
|
+
"draftBody": "My reply content...", // Optional comment
|
|
332
|
+
"replyToAll": false
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create a forward draft (automatically includes original content)
|
|
337
|
+
{
|
|
338
|
+
"tool": "manage_email",
|
|
339
|
+
"arguments": {
|
|
340
|
+
"action": "forward_draft",
|
|
341
|
+
"originalMessageId": "original_email_id",
|
|
342
|
+
"draftBody": "Forwarding this for your review..." // Optional comment
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update an existing draft
|
|
347
|
+
{
|
|
348
|
+
"tool": "manage_email",
|
|
349
|
+
"arguments": {
|
|
350
|
+
"action": "update_draft",
|
|
351
|
+
"draftId": "draft_email_id",
|
|
352
|
+
"draftSubject": "Updated Subject",
|
|
353
|
+
"draftBody": "Updated content..."
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Send a saved draft
|
|
358
|
+
{
|
|
359
|
+
"tool": "manage_email",
|
|
360
|
+
"arguments": {
|
|
361
|
+
"action": "send_draft",
|
|
362
|
+
"draftId": "draft_email_id"
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// List all draft emails
|
|
367
|
+
{
|
|
368
|
+
"tool": "manage_email",
|
|
369
|
+
"arguments": {
|
|
370
|
+
"action": "list_drafts",
|
|
371
|
+
"maxResults": 20
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Folder Operations
|
|
377
|
+
```javascript
|
|
378
|
+
// List all folders including subfolders with hierarchy
|
|
379
|
+
{
|
|
380
|
+
"tool": "list_folders"
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// List only child folders of a specific parent folder
|
|
384
|
+
{
|
|
385
|
+
"tool": "list_folders",
|
|
386
|
+
"arguments": {
|
|
387
|
+
"parentFolderId": "inbox_folder_id"
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Search for folders by name (case-insensitive)
|
|
392
|
+
{
|
|
393
|
+
"tool": "list_folders",
|
|
394
|
+
"arguments": {
|
|
395
|
+
"searchName": "projects"
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
311
400
|
## 🎯 Command Line Options
|
|
312
401
|
|
|
313
402
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -67,7 +67,7 @@ function parseArgs() {
|
|
|
67
67
|
}
|
|
68
68
|
const server = new Server({
|
|
69
69
|
name: "ms365-mcp-server",
|
|
70
|
-
version: "1.1.
|
|
70
|
+
version: "1.1.10"
|
|
71
71
|
}, {
|
|
72
72
|
capabilities: {
|
|
73
73
|
resources: {
|
|
@@ -195,8 +195,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
195
195
|
},
|
|
196
196
|
action: {
|
|
197
197
|
type: "string",
|
|
198
|
-
enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me", "draft"],
|
|
199
|
-
description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create/save draft)"
|
|
198
|
+
enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me", "draft", "update_draft", "send_draft", "list_drafts", "reply_draft", "forward_draft"],
|
|
199
|
+
description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create/save draft), update_draft (modify existing draft), send_draft (send saved draft), list_drafts (list draft emails), reply_draft (create reply draft), forward_draft (create forward draft)"
|
|
200
200
|
},
|
|
201
201
|
messageId: {
|
|
202
202
|
type: "string",
|
|
@@ -329,6 +329,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
329
329
|
required: ["name", "contentBytes"]
|
|
330
330
|
},
|
|
331
331
|
description: "List of email attachments for draft (optional)"
|
|
332
|
+
},
|
|
333
|
+
// Additional draft parameters
|
|
334
|
+
draftId: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "Draft email ID (required for update_draft and send_draft actions)"
|
|
337
|
+
},
|
|
338
|
+
originalMessageId: {
|
|
339
|
+
type: "string",
|
|
340
|
+
description: "Original message ID for reply_draft and forward_draft actions"
|
|
341
|
+
},
|
|
342
|
+
replyToAll: {
|
|
343
|
+
type: "boolean",
|
|
344
|
+
description: "Reply to all recipients (used with reply_draft action)",
|
|
345
|
+
default: false
|
|
346
|
+
},
|
|
347
|
+
conversationId: {
|
|
348
|
+
type: "string",
|
|
349
|
+
description: "Conversation ID for threading drafts (optional)"
|
|
332
350
|
}
|
|
333
351
|
},
|
|
334
352
|
required: ["action"],
|
|
@@ -360,13 +378,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
360
378
|
},
|
|
361
379
|
{
|
|
362
380
|
name: "list_folders",
|
|
363
|
-
description: "List all mail folders in the mailbox. Returns folder names, IDs, and item counts for navigation and organization.",
|
|
381
|
+
description: "List all mail folders in the mailbox including subfolders. Returns folder hierarchy with names, IDs, paths, and item counts for navigation and organization. Now includes child folders inside parent folders like Inbox subfolders.",
|
|
364
382
|
inputSchema: {
|
|
365
383
|
type: "object",
|
|
366
384
|
properties: {
|
|
367
385
|
userId: {
|
|
368
386
|
type: "string",
|
|
369
387
|
description: "User ID for multi-user authentication (required if using multi-user mode)"
|
|
388
|
+
},
|
|
389
|
+
parentFolderId: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "Optional: List only child folders of this parent folder ID. If not specified, returns complete folder hierarchy."
|
|
392
|
+
},
|
|
393
|
+
searchName: {
|
|
394
|
+
type: "string",
|
|
395
|
+
description: "Optional: Search for folders by name (case-insensitive partial match)"
|
|
370
396
|
}
|
|
371
397
|
},
|
|
372
398
|
additionalProperties: false
|
|
@@ -822,7 +848,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
822
848
|
body: args.draftBody,
|
|
823
849
|
bodyType: args.draftBodyType || 'text',
|
|
824
850
|
importance: args.draftImportance || 'normal',
|
|
825
|
-
attachments: args.draftAttachments
|
|
851
|
+
attachments: args.draftAttachments,
|
|
852
|
+
conversationId: args.conversationId
|
|
826
853
|
});
|
|
827
854
|
return {
|
|
828
855
|
content: [
|
|
@@ -832,6 +859,85 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
832
859
|
}
|
|
833
860
|
]
|
|
834
861
|
};
|
|
862
|
+
case "update_draft":
|
|
863
|
+
if (!args?.draftId) {
|
|
864
|
+
throw new Error("draftId is required for update_draft action");
|
|
865
|
+
}
|
|
866
|
+
const updates = {};
|
|
867
|
+
if (args.draftTo)
|
|
868
|
+
updates.to = args.draftTo;
|
|
869
|
+
if (args.draftCc)
|
|
870
|
+
updates.cc = args.draftCc;
|
|
871
|
+
if (args.draftBcc)
|
|
872
|
+
updates.bcc = args.draftBcc;
|
|
873
|
+
if (args.draftSubject)
|
|
874
|
+
updates.subject = args.draftSubject;
|
|
875
|
+
if (args.draftBody)
|
|
876
|
+
updates.body = args.draftBody;
|
|
877
|
+
if (args.draftBodyType)
|
|
878
|
+
updates.bodyType = args.draftBodyType;
|
|
879
|
+
if (args.draftImportance)
|
|
880
|
+
updates.importance = args.draftImportance;
|
|
881
|
+
if (args.draftAttachments)
|
|
882
|
+
updates.attachments = args.draftAttachments;
|
|
883
|
+
const updateResult = await ms365Ops.updateDraftEmail(args.draftId, updates);
|
|
884
|
+
return {
|
|
885
|
+
content: [
|
|
886
|
+
{
|
|
887
|
+
type: "text",
|
|
888
|
+
text: `✅ Draft email updated successfully!\n🆔 Draft ID: ${updateResult.id}\n📝 Status: ${updateResult.status}`
|
|
889
|
+
}
|
|
890
|
+
]
|
|
891
|
+
};
|
|
892
|
+
case "send_draft":
|
|
893
|
+
if (!args?.draftId) {
|
|
894
|
+
throw new Error("draftId is required for send_draft action");
|
|
895
|
+
}
|
|
896
|
+
const sendResult = await ms365Ops.sendDraftEmail(args.draftId);
|
|
897
|
+
return {
|
|
898
|
+
content: [
|
|
899
|
+
{
|
|
900
|
+
type: "text",
|
|
901
|
+
text: `✅ Draft email sent successfully!\n🆔 Draft ID: ${sendResult.id}\n📤 Status: ${sendResult.status}`
|
|
902
|
+
}
|
|
903
|
+
]
|
|
904
|
+
};
|
|
905
|
+
case "list_drafts":
|
|
906
|
+
const draftsList = await ms365Ops.listDrafts(args?.maxResults || 50);
|
|
907
|
+
return {
|
|
908
|
+
content: [
|
|
909
|
+
{
|
|
910
|
+
type: "text",
|
|
911
|
+
text: `📝 Draft Emails (${draftsList.messages.length} found)\n\n${draftsList.messages.map((draft) => `📝 ${draft.subject || 'No subject'}\n 👥 To: ${draft.toRecipients?.map((r) => r.address).join(', ') || 'No recipients'}\n 🆔 ID: ${draft.id}\n 📅 Created: ${new Date(draft.receivedDateTime || draft.sentDateTime).toLocaleDateString()}\n 💬 Conversation: ${draft.conversationId || 'None'}\n`).join('\n')}\n${draftsList.hasMore ? '➡️ More drafts available - use maxResults to see more' : ''}`
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
};
|
|
915
|
+
case "reply_draft":
|
|
916
|
+
if (!args?.originalMessageId) {
|
|
917
|
+
throw new Error("originalMessageId is required for reply_draft action");
|
|
918
|
+
}
|
|
919
|
+
const replyDraftResult = await ms365Ops.createReplyDraft(args.originalMessageId, args.draftBody, args.replyToAll || false);
|
|
920
|
+
return {
|
|
921
|
+
content: [
|
|
922
|
+
{
|
|
923
|
+
type: "text",
|
|
924
|
+
text: `✅ Reply draft created successfully!\n📧 Original Message: ${args.originalMessageId}\n💬 Reply Type: ${args.replyToAll ? 'Reply All' : 'Reply'}\n🆔 Draft ID: ${replyDraftResult.id}\n💬 Conversation ID: ${replyDraftResult.conversationId}\n📧 To: ${replyDraftResult.toRecipients?.join(', ') || 'Auto-determined'}`
|
|
925
|
+
}
|
|
926
|
+
]
|
|
927
|
+
};
|
|
928
|
+
case "forward_draft":
|
|
929
|
+
if (!args?.originalMessageId) {
|
|
930
|
+
throw new Error("originalMessageId is required for forward_draft action");
|
|
931
|
+
}
|
|
932
|
+
const forwardDraftResult = await ms365Ops.createForwardDraft(args.originalMessageId, args.draftBody);
|
|
933
|
+
return {
|
|
934
|
+
content: [
|
|
935
|
+
{
|
|
936
|
+
type: "text",
|
|
937
|
+
text: `✅ Forward draft created successfully!\n📧 Original Message: ${args.originalMessageId}\n🆔 Draft ID: ${forwardDraftResult.id}\n💬 Conversation ID: ${forwardDraftResult.conversationId}\n📋 Subject: ${forwardDraftResult.subject}`
|
|
938
|
+
}
|
|
939
|
+
]
|
|
940
|
+
};
|
|
835
941
|
default:
|
|
836
942
|
throw new Error(`Unknown email action: ${emailAction}`);
|
|
837
943
|
}
|
|
@@ -986,12 +1092,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
986
1092
|
const graphClient = await enhancedMS365Auth.getGraphClient();
|
|
987
1093
|
ms365Ops.setGraphClient(graphClient);
|
|
988
1094
|
}
|
|
989
|
-
|
|
1095
|
+
let folders;
|
|
1096
|
+
let resultTitle = "📁 Mail Folders";
|
|
1097
|
+
if (args?.parentFolderId) {
|
|
1098
|
+
// List only child folders of specified parent
|
|
1099
|
+
const parentFolderId = args.parentFolderId;
|
|
1100
|
+
folders = await ms365Ops.listChildFolders(parentFolderId);
|
|
1101
|
+
resultTitle = `📁 Child Folders of ${parentFolderId}`;
|
|
1102
|
+
}
|
|
1103
|
+
else if (args?.searchName) {
|
|
1104
|
+
// Search for folders by name
|
|
1105
|
+
const searchName = args.searchName;
|
|
1106
|
+
folders = await ms365Ops.findFolderByName(searchName);
|
|
1107
|
+
resultTitle = `📁 Folders matching "${searchName}"`;
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
// List all folders with hierarchy
|
|
1111
|
+
folders = await ms365Ops.listFolders();
|
|
1112
|
+
resultTitle = "📁 Mail Folders (Complete Hierarchy)";
|
|
1113
|
+
}
|
|
1114
|
+
// Format folders with proper hierarchy display
|
|
1115
|
+
const formatFolder = (folder) => {
|
|
1116
|
+
const indent = ' '.repeat(folder.depth || 0);
|
|
1117
|
+
const icon = folder.depth === 0 ? '📁' : '📂';
|
|
1118
|
+
const pathInfo = folder.fullPath ? `\n${indent} 📍 Path: ${folder.fullPath}` : '';
|
|
1119
|
+
return `${indent}${icon} ${folder.displayName}\n${indent} 🆔 ID: ${folder.id}\n${indent} 📧 Total Items: ${folder.totalItemCount}\n${indent} 📩 Unread: ${folder.unreadItemCount}${pathInfo}\n`;
|
|
1120
|
+
};
|
|
1121
|
+
const folderCount = folders.length;
|
|
1122
|
+
const totalItems = folders.reduce((sum, folder) => sum + (folder.totalItemCount || 0), 0);
|
|
1123
|
+
const totalUnread = folders.reduce((sum, folder) => sum + (folder.unreadItemCount || 0), 0);
|
|
990
1124
|
return {
|
|
991
1125
|
content: [
|
|
992
1126
|
{
|
|
993
1127
|
type: "text",
|
|
994
|
-
text:
|
|
1128
|
+
text: `${resultTitle}\n\n📊 Summary: ${folderCount} folders, ${totalItems} total items, ${totalUnread} unread\n\n${folders.map(formatFolder).join('\n')}`
|
|
995
1129
|
}
|
|
996
1130
|
]
|
|
997
1131
|
};
|
|
@@ -290,6 +290,19 @@ export class MS365Operations {
|
|
|
290
290
|
importance: message.importance || 'normal',
|
|
291
291
|
attachments: attachments.length > 0 ? attachments : undefined
|
|
292
292
|
};
|
|
293
|
+
// Handle threading - set conversationId for replies/forwards
|
|
294
|
+
if (message.conversationId) {
|
|
295
|
+
draftBody.conversationId = message.conversationId;
|
|
296
|
+
}
|
|
297
|
+
// Handle in-reply-to for proper threading
|
|
298
|
+
if (message.inReplyTo) {
|
|
299
|
+
draftBody.internetMessageHeaders = [
|
|
300
|
+
{
|
|
301
|
+
name: 'In-Reply-To',
|
|
302
|
+
value: message.inReplyTo
|
|
303
|
+
}
|
|
304
|
+
];
|
|
305
|
+
}
|
|
293
306
|
if (message.replyTo) {
|
|
294
307
|
draftBody.replyTo = [{
|
|
295
308
|
emailAddress: {
|
|
@@ -313,6 +326,211 @@ export class MS365Operations {
|
|
|
313
326
|
throw error;
|
|
314
327
|
}
|
|
315
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Update a draft email
|
|
331
|
+
*/
|
|
332
|
+
async updateDraftEmail(draftId, updates) {
|
|
333
|
+
try {
|
|
334
|
+
const graphClient = await this.getGraphClient();
|
|
335
|
+
// Prepare update payload
|
|
336
|
+
const updateBody = {};
|
|
337
|
+
if (updates.subject) {
|
|
338
|
+
updateBody.subject = updates.subject;
|
|
339
|
+
}
|
|
340
|
+
if (updates.body) {
|
|
341
|
+
updateBody.body = {
|
|
342
|
+
contentType: updates.bodyType === 'html' ? 'html' : 'text',
|
|
343
|
+
content: updates.body
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (updates.to) {
|
|
347
|
+
updateBody.toRecipients = updates.to.map(email => ({
|
|
348
|
+
emailAddress: {
|
|
349
|
+
address: email,
|
|
350
|
+
name: email.split('@')[0]
|
|
351
|
+
}
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
if (updates.cc) {
|
|
355
|
+
updateBody.ccRecipients = updates.cc.map(email => ({
|
|
356
|
+
emailAddress: {
|
|
357
|
+
address: email,
|
|
358
|
+
name: email.split('@')[0]
|
|
359
|
+
}
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
if (updates.bcc) {
|
|
363
|
+
updateBody.bccRecipients = updates.bcc.map(email => ({
|
|
364
|
+
emailAddress: {
|
|
365
|
+
address: email,
|
|
366
|
+
name: email.split('@')[0]
|
|
367
|
+
}
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
if (updates.importance) {
|
|
371
|
+
updateBody.importance = updates.importance;
|
|
372
|
+
}
|
|
373
|
+
if (updates.replyTo) {
|
|
374
|
+
updateBody.replyTo = [{
|
|
375
|
+
emailAddress: {
|
|
376
|
+
address: updates.replyTo,
|
|
377
|
+
name: updates.replyTo.split('@')[0]
|
|
378
|
+
}
|
|
379
|
+
}];
|
|
380
|
+
}
|
|
381
|
+
// Update attachments if provided
|
|
382
|
+
if (updates.attachments) {
|
|
383
|
+
updateBody.attachments = updates.attachments.map(att => ({
|
|
384
|
+
'@odata.type': '#microsoft.graph.fileAttachment',
|
|
385
|
+
name: att.name,
|
|
386
|
+
contentBytes: att.contentBytes,
|
|
387
|
+
contentType: att.contentType || 'application/octet-stream'
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
// Update the draft
|
|
391
|
+
const result = await graphClient
|
|
392
|
+
.api(`/me/messages/${draftId}`)
|
|
393
|
+
.patch(updateBody);
|
|
394
|
+
logger.log(`Draft email ${draftId} updated successfully`);
|
|
395
|
+
return {
|
|
396
|
+
id: result.id || draftId,
|
|
397
|
+
status: 'draft_updated'
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
logger.error(`Error updating draft email ${draftId}:`, error);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Send a draft email
|
|
407
|
+
*/
|
|
408
|
+
async sendDraftEmail(draftId) {
|
|
409
|
+
try {
|
|
410
|
+
const graphClient = await this.getGraphClient();
|
|
411
|
+
// Send the draft
|
|
412
|
+
await graphClient
|
|
413
|
+
.api(`/me/messages/${draftId}/send`)
|
|
414
|
+
.post({});
|
|
415
|
+
logger.log(`Draft email ${draftId} sent successfully`);
|
|
416
|
+
return {
|
|
417
|
+
id: draftId,
|
|
418
|
+
status: 'sent'
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
logger.error(`Error sending draft email ${draftId}:`, error);
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* List draft emails
|
|
428
|
+
*/
|
|
429
|
+
async listDrafts(maxResults = 50) {
|
|
430
|
+
try {
|
|
431
|
+
const graphClient = await this.getGraphClient();
|
|
432
|
+
const result = await graphClient
|
|
433
|
+
.api('/me/mailFolders/drafts/messages')
|
|
434
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink,isDraft')
|
|
435
|
+
.orderby('createdDateTime desc')
|
|
436
|
+
.top(maxResults)
|
|
437
|
+
.get();
|
|
438
|
+
const messages = result.value?.map((email) => ({
|
|
439
|
+
id: email.id,
|
|
440
|
+
subject: email.subject || '',
|
|
441
|
+
from: {
|
|
442
|
+
name: email.from?.emailAddress?.name || '',
|
|
443
|
+
address: email.from?.emailAddress?.address || ''
|
|
444
|
+
},
|
|
445
|
+
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
446
|
+
name: recipient.emailAddress?.name || '',
|
|
447
|
+
address: recipient.emailAddress?.address || ''
|
|
448
|
+
})) || [],
|
|
449
|
+
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
450
|
+
name: recipient.emailAddress?.name || '',
|
|
451
|
+
address: recipient.emailAddress?.address || ''
|
|
452
|
+
})) || [],
|
|
453
|
+
receivedDateTime: email.receivedDateTime,
|
|
454
|
+
sentDateTime: email.sentDateTime,
|
|
455
|
+
bodyPreview: email.bodyPreview || '',
|
|
456
|
+
isRead: email.isRead || false,
|
|
457
|
+
hasAttachments: email.hasAttachments || false,
|
|
458
|
+
importance: email.importance || 'normal',
|
|
459
|
+
conversationId: email.conversationId || '',
|
|
460
|
+
parentFolderId: email.parentFolderId || '',
|
|
461
|
+
webLink: email.webLink || '',
|
|
462
|
+
attachments: []
|
|
463
|
+
})) || [];
|
|
464
|
+
return {
|
|
465
|
+
messages,
|
|
466
|
+
hasMore: !!result['@odata.nextLink']
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
logger.error('Error listing draft emails:', error);
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Create a threaded reply draft from a specific message
|
|
476
|
+
*/
|
|
477
|
+
async createReplyDraft(originalMessageId, body, replyToAll = false) {
|
|
478
|
+
try {
|
|
479
|
+
const graphClient = await this.getGraphClient();
|
|
480
|
+
// Use Microsoft Graph's createReply endpoint for proper threading
|
|
481
|
+
const endpoint = replyToAll
|
|
482
|
+
? `/me/messages/${originalMessageId}/createReplyAll`
|
|
483
|
+
: `/me/messages/${originalMessageId}/createReply`;
|
|
484
|
+
const requestBody = {};
|
|
485
|
+
// If body is provided, include it as a comment
|
|
486
|
+
if (body) {
|
|
487
|
+
requestBody.comment = body;
|
|
488
|
+
}
|
|
489
|
+
const replyDraft = await graphClient
|
|
490
|
+
.api(endpoint)
|
|
491
|
+
.post(requestBody);
|
|
492
|
+
return {
|
|
493
|
+
id: replyDraft.id,
|
|
494
|
+
subject: replyDraft.subject,
|
|
495
|
+
conversationId: replyDraft.conversationId,
|
|
496
|
+
toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
|
|
497
|
+
ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
|
|
498
|
+
bodyPreview: replyDraft.bodyPreview,
|
|
499
|
+
isDraft: replyDraft.isDraft
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
throw new Error(`Error creating reply draft: ${error}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Create a threaded forward draft from a specific message
|
|
508
|
+
*/
|
|
509
|
+
async createForwardDraft(originalMessageId, comment) {
|
|
510
|
+
try {
|
|
511
|
+
const graphClient = await this.getGraphClient();
|
|
512
|
+
// Use Microsoft Graph's createForward endpoint for proper threading
|
|
513
|
+
const endpoint = `/me/messages/${originalMessageId}/createForward`;
|
|
514
|
+
const requestBody = {};
|
|
515
|
+
// If comment is provided, include it
|
|
516
|
+
if (comment) {
|
|
517
|
+
requestBody.comment = comment;
|
|
518
|
+
}
|
|
519
|
+
const forwardDraft = await graphClient
|
|
520
|
+
.api(endpoint)
|
|
521
|
+
.post(requestBody);
|
|
522
|
+
return {
|
|
523
|
+
id: forwardDraft.id,
|
|
524
|
+
subject: forwardDraft.subject,
|
|
525
|
+
conversationId: forwardDraft.conversationId,
|
|
526
|
+
bodyPreview: forwardDraft.bodyPreview,
|
|
527
|
+
isDraft: forwardDraft.isDraft
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
throw new Error(`Error creating forward draft: ${error}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
316
534
|
/**
|
|
317
535
|
* Get email by ID
|
|
318
536
|
*/
|
|
@@ -732,15 +950,91 @@ export class MS365Operations {
|
|
|
732
950
|
}
|
|
733
951
|
}
|
|
734
952
|
/**
|
|
735
|
-
* List mail folders
|
|
953
|
+
* List mail folders including child folders recursively
|
|
736
954
|
*/
|
|
737
955
|
async listFolders() {
|
|
738
956
|
try {
|
|
739
957
|
const graphClient = await this.getGraphClient();
|
|
958
|
+
const allFolders = [];
|
|
959
|
+
// Get top-level folders
|
|
740
960
|
const result = await graphClient
|
|
741
961
|
.api('/me/mailFolders')
|
|
742
962
|
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
743
963
|
.get();
|
|
964
|
+
// Process top-level folders
|
|
965
|
+
const topLevelFolders = result.value?.map((folder) => ({
|
|
966
|
+
id: folder.id,
|
|
967
|
+
displayName: folder.displayName || '',
|
|
968
|
+
totalItemCount: folder.totalItemCount || 0,
|
|
969
|
+
unreadItemCount: folder.unreadItemCount || 0,
|
|
970
|
+
parentFolderId: folder.parentFolderId,
|
|
971
|
+
depth: 0,
|
|
972
|
+
fullPath: folder.displayName || ''
|
|
973
|
+
})) || [];
|
|
974
|
+
allFolders.push(...topLevelFolders);
|
|
975
|
+
// Recursively get child folders for each top-level folder
|
|
976
|
+
for (const folder of topLevelFolders) {
|
|
977
|
+
const childFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, 1);
|
|
978
|
+
allFolders.push(...childFolders);
|
|
979
|
+
}
|
|
980
|
+
// Sort folders by full path for better organization
|
|
981
|
+
allFolders.sort((a, b) => (a.fullPath || '').localeCompare(b.fullPath || ''));
|
|
982
|
+
return allFolders;
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
logger.error('Error listing folders:', error);
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Recursively get child folders of a parent folder
|
|
991
|
+
*/
|
|
992
|
+
async getChildFolders(parentFolderId, parentPath, depth) {
|
|
993
|
+
try {
|
|
994
|
+
const graphClient = await this.getGraphClient();
|
|
995
|
+
const childFolders = [];
|
|
996
|
+
// Get child folders of the specified parent
|
|
997
|
+
const result = await graphClient
|
|
998
|
+
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
999
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1000
|
+
.get();
|
|
1001
|
+
if (result.value && result.value.length > 0) {
|
|
1002
|
+
const folders = result.value.map((folder) => ({
|
|
1003
|
+
id: folder.id,
|
|
1004
|
+
displayName: folder.displayName || '',
|
|
1005
|
+
totalItemCount: folder.totalItemCount || 0,
|
|
1006
|
+
unreadItemCount: folder.unreadItemCount || 0,
|
|
1007
|
+
parentFolderId: folder.parentFolderId,
|
|
1008
|
+
depth,
|
|
1009
|
+
fullPath: `${parentPath}/${folder.displayName || ''}`
|
|
1010
|
+
}));
|
|
1011
|
+
childFolders.push(...folders);
|
|
1012
|
+
// Recursively get child folders (limit depth to prevent infinite recursion)
|
|
1013
|
+
if (depth < 10) { // Max depth of 10 levels
|
|
1014
|
+
for (const folder of folders) {
|
|
1015
|
+
const subChildFolders = await this.getChildFolders(folder.id, folder.fullPath || folder.displayName, depth + 1);
|
|
1016
|
+
childFolders.push(...subChildFolders);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return childFolders;
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
// Log the error but don't throw - some folders might not have children or access might be restricted
|
|
1024
|
+
logger.log(`Could not access child folders for ${parentFolderId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1025
|
+
return [];
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* List child folders of a specific parent folder
|
|
1030
|
+
*/
|
|
1031
|
+
async listChildFolders(parentFolderId) {
|
|
1032
|
+
try {
|
|
1033
|
+
const graphClient = await this.getGraphClient();
|
|
1034
|
+
const result = await graphClient
|
|
1035
|
+
.api(`/me/mailFolders/${parentFolderId}/childFolders`)
|
|
1036
|
+
.select('id,displayName,totalItemCount,unreadItemCount,parentFolderId')
|
|
1037
|
+
.get();
|
|
744
1038
|
return result.value?.map((folder) => ({
|
|
745
1039
|
id: folder.id,
|
|
746
1040
|
displayName: folder.displayName || '',
|
|
@@ -750,7 +1044,22 @@ export class MS365Operations {
|
|
|
750
1044
|
})) || [];
|
|
751
1045
|
}
|
|
752
1046
|
catch (error) {
|
|
753
|
-
logger.error(
|
|
1047
|
+
logger.error(`Error listing child folders for ${parentFolderId}:`, error);
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Find folder by name (case-insensitive search across all folders)
|
|
1053
|
+
*/
|
|
1054
|
+
async findFolderByName(folderName) {
|
|
1055
|
+
try {
|
|
1056
|
+
const allFolders = await this.listFolders();
|
|
1057
|
+
const searchName = folderName.toLowerCase();
|
|
1058
|
+
return allFolders.filter(folder => folder.displayName.toLowerCase().includes(searchName) ||
|
|
1059
|
+
(folder.fullPath && folder.fullPath.toLowerCase().includes(searchName)));
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
logger.error(`Error finding folder by name ${folderName}:`, error);
|
|
754
1063
|
throw error;
|
|
755
1064
|
}
|
|
756
1065
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ms365-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|