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 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.9"
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
- const folders = await ms365Ops.listFolders();
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: `📁 Mail Folders\n\n${folders.map((folder) => `📂 ${folder.displayName}\n 🆔 ID: ${folder.id}\n 📧 Total Items: ${folder.totalItemCount}\n 📩 Unread: ${folder.unreadItemCount}\n`).join('\n')}`
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('Error listing folders:', 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.9",
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",