ms365-mcp-server 1.1.11 → 1.1.12

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/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.11"
70
+ version: "1.1.12"
71
71
  }, {
72
72
  capabilities: {
73
73
  resources: {
@@ -83,6 +83,7 @@ const server = new Server({
83
83
  }
84
84
  }
85
85
  });
86
+ logger.log('Server started with version 1.1.12');
86
87
  // Set up the resource listing request handler
87
88
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
88
89
  logger.log('Received list resources request');
@@ -185,7 +186,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
185
186
  },
186
187
  {
187
188
  name: "manage_email",
188
- description: "UNIFIED EMAIL MANAGEMENT: Read, search, list, mark, move, or delete emails. Combines all email operations in one powerful tool. Supports intelligent partial name matching, folder management, and advanced search criteria.",
189
+ description: "UNIFIED EMAIL MANAGEMENT: Read, search, list, mark, move, or delete emails. Combines all email operations in one powerful tool. Supports intelligent partial name matching, folder management, and advanced search criteria. IMPORTANT: Use 'reply_draft' or 'forward_draft' actions (not 'draft') to create threaded drafts that appear in email conversations.",
189
190
  inputSchema: {
190
191
  type: "object",
191
192
  properties: {
@@ -196,7 +197,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
196
197
  action: {
197
198
  type: "string",
198
199
  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
+ 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 standalone draft - NOTE: Use reply_draft/forward_draft for threaded drafts), update_draft (modify existing draft), send_draft (send saved draft), list_drafts (list draft emails), reply_draft (create threaded reply draft that appears in conversation), forward_draft (create threaded forward draft that appears in conversation)"
200
201
  },
201
202
  messageId: {
202
203
  type: "string",
@@ -631,6 +632,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
631
632
  }
632
633
  // ============ UNIFIED EMAIL MANAGEMENT TOOL ============
633
634
  case "manage_email":
635
+ // Debug logging for parameter validation
636
+ logger.log('DEBUG: manage_email called with args:', JSON.stringify(args, null, 2));
634
637
  if (ms365Config.multiUser) {
635
638
  const userId = args?.userId;
636
639
  if (!userId) {
@@ -644,6 +647,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
644
647
  ms365Ops.setGraphClient(graphClient);
645
648
  }
646
649
  const emailAction = args?.action;
650
+ logger.log(`DEBUG: Email action: ${emailAction}`);
647
651
  switch (emailAction) {
648
652
  case "read":
649
653
  if (!args?.messageId) {
@@ -681,6 +685,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
681
685
  };
682
686
  case "search":
683
687
  const searchResults = await ms365Ops.searchEmails(args);
688
+ logger.log(`DEBUG: Search results count: ${searchResults.messages.length}`);
689
+ logger.log(`DEBUG: First result:`, JSON.stringify(searchResults.messages[0], null, 2));
684
690
  // Enhanced feedback for search results
685
691
  let responseText = `šŸ” Email Search Results (${searchResults.messages.length} found)`;
686
692
  if (searchResults.messages.length === 0) {
@@ -698,7 +704,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
698
704
  responseText += `• Remove some search criteria to get broader results`;
699
705
  }
700
706
  else {
701
- responseText += `\n\n${searchResults.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}`;
707
+ responseText += `\n\n${searchResults.messages.map((email, index) => {
708
+ // Handle missing IDs more gracefully
709
+ const emailId = email.id || 'ID_MISSING';
710
+ const fromDisplay = email.from?.name ? `${email.from.name} <${email.from.address}>` : email.from?.address || 'Unknown sender';
711
+ return `${index + 1}. šŸ“§ ${email.subject || 'No subject'}\n šŸ‘¤ From: ${fromDisplay}\n šŸ“… ${email.receivedDateTime ? new Date(email.receivedDateTime).toLocaleDateString() : 'Unknown date'}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${emailId}\n`;
712
+ }).join('\n')}`;
702
713
  if (searchResults.hasMore) {
703
714
  responseText += `\nšŸ’” There are more results available. Use maxResults parameter to get more emails.`;
704
715
  }
@@ -840,6 +851,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
840
851
  if (!args?.draftTo || !args?.draftSubject || !args?.draftBody) {
841
852
  throw new Error("draftTo, draftSubject, and draftBody are required for draft action");
842
853
  }
854
+ // Check if this looks like a reply based on subject
855
+ const isLikelyReply = args.draftSubject?.toString().toLowerCase().startsWith('re:');
856
+ if (isLikelyReply) {
857
+ logger.log('šŸ”” SUGGESTION: Consider using "reply_draft" action instead of "draft" for threaded replies that appear in conversations');
858
+ }
843
859
  const draftResult = await ms365Ops.saveDraftEmail({
844
860
  to: args.draftTo,
845
861
  cc: args.draftCc,
@@ -851,11 +867,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
851
867
  attachments: args.draftAttachments,
852
868
  conversationId: args.conversationId
853
869
  });
870
+ let draftResponseText = `āœ… Draft email saved successfully!\nšŸ“§ Subject: ${args.draftSubject}\nšŸ‘„ To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\nšŸ†” Draft ID: ${draftResult.id}`;
871
+ // Add helpful suggestion for threaded drafts
872
+ if (isLikelyReply) {
873
+ draftResponseText += `\n\nšŸ’” TIP: This looks like a reply email. For drafts that appear in email threads, use "reply_draft" action with the original message ID instead of "draft".`;
874
+ }
854
875
  return {
855
876
  content: [
856
877
  {
857
878
  type: "text",
858
- text: `āœ… Draft email saved successfully!\nšŸ“§ Subject: ${args.draftSubject}\nšŸ‘„ To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\nšŸ†” Draft ID: ${draftResult.id}`
879
+ text: draftResponseText
859
880
  }
860
881
  ]
861
882
  };
@@ -423,6 +423,54 @@ export class MS365Operations {
423
423
  throw error;
424
424
  }
425
425
  }
426
+ /**
427
+ * Verify draft threading by checking if draft appears in conversation
428
+ */
429
+ async verifyDraftThreading(draftId, originalConversationId) {
430
+ try {
431
+ const graphClient = await this.getGraphClient();
432
+ // Get the draft details
433
+ const draft = await graphClient
434
+ .api(`/me/messages/${draftId}`)
435
+ .select('id,subject,conversationId,parentFolderId,internetMessageHeaders,isDraft')
436
+ .get();
437
+ // Check if conversation IDs match
438
+ const conversationMatch = draft.conversationId === originalConversationId;
439
+ // Get all messages in the conversation to see if draft appears
440
+ let conversationMessages = [];
441
+ try {
442
+ const convResult = await graphClient
443
+ .api('/me/messages')
444
+ .filter(`conversationId eq '${originalConversationId}'`)
445
+ .select('id,subject,isDraft,parentFolderId')
446
+ .get();
447
+ conversationMessages = convResult.value || [];
448
+ }
449
+ catch (convError) {
450
+ logger.log(`Could not fetch conversation messages: ${convError}`);
451
+ }
452
+ const draftInConversation = conversationMessages.some((msg) => msg.id === draftId);
453
+ return {
454
+ isThreaded: conversationMatch && draftInConversation,
455
+ details: {
456
+ draftConversationId: draft.conversationId,
457
+ originalConversationId,
458
+ conversationMatch,
459
+ draftInConversation,
460
+ draftFolder: draft.parentFolderId,
461
+ conversationMessagesCount: conversationMessages.length,
462
+ internetMessageHeaders: draft.internetMessageHeaders
463
+ }
464
+ };
465
+ }
466
+ catch (error) {
467
+ logger.error('Error verifying draft threading:', error);
468
+ return {
469
+ isThreaded: false,
470
+ details: { error: String(error) }
471
+ };
472
+ }
473
+ }
426
474
  /**
427
475
  * List draft emails
428
476
  */
@@ -477,27 +525,143 @@ export class MS365Operations {
477
525
  async createReplyDraft(originalMessageId, body, replyToAll = false) {
478
526
  try {
479
527
  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
- };
528
+ logger.log(`Creating reply draft for message: ${originalMessageId}`);
529
+ logger.log(`Reply to all: ${replyToAll}`);
530
+ // First, get the original message to include its content in the reply
531
+ const originalMessage = await graphClient
532
+ .api(`/me/messages/${originalMessageId}`)
533
+ .select('id,subject,from,toRecipients,ccRecipients,conversationId,parentFolderId,internetMessageId,internetMessageHeaders,conversationIndex,body,sentDateTime')
534
+ .get();
535
+ // Build the complete reply body with original content
536
+ const originalBodyContent = originalMessage.body?.content || '';
537
+ const fromDisplay = originalMessage.from?.emailAddress?.name || originalMessage.from?.emailAddress?.address || '';
538
+ const sentDate = new Date(originalMessage.sentDateTime).toLocaleString();
539
+ const completeReplyBody = `${body || ''}
540
+
541
+ <br><br>
542
+ <div style="border-left: 2px solid #ccc; padding-left: 10px; margin-top: 10px;">
543
+ <p><strong>From:</strong> ${fromDisplay}<br>
544
+ <strong>Sent:</strong> ${sentDate}<br>
545
+ <strong>Subject:</strong> ${originalMessage.subject}</p>
546
+ <hr style="border: none; border-top: 1px solid #ccc; margin: 10px 0;">
547
+ ${originalBodyContent}
548
+ </div>`;
549
+ // First, try using the official Microsoft Graph createReply endpoint for proper threading
550
+ try {
551
+ const endpoint = replyToAll
552
+ ? `/me/messages/${originalMessageId}/createReplyAll`
553
+ : `/me/messages/${originalMessageId}/createReply`;
554
+ logger.log(`Using official Graph API endpoint: ${endpoint}`);
555
+ const replyDraft = await graphClient
556
+ .api(endpoint)
557
+ .post({
558
+ message: {
559
+ body: {
560
+ contentType: 'html',
561
+ content: completeReplyBody
562
+ }
563
+ }
564
+ });
565
+ logger.log(`Reply draft created successfully with ID: ${replyDraft.id}`);
566
+ logger.log(`Draft conversation ID: ${replyDraft.conversationId}`);
567
+ logger.log(`Draft appears as threaded reply in conversation with original content`);
568
+ return {
569
+ id: replyDraft.id,
570
+ subject: replyDraft.subject,
571
+ conversationId: replyDraft.conversationId,
572
+ toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
573
+ ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
574
+ bodyPreview: replyDraft.bodyPreview,
575
+ isDraft: replyDraft.isDraft,
576
+ parentFolderId: replyDraft.parentFolderId
577
+ };
578
+ }
579
+ catch (officialApiError) {
580
+ logger.log(`Official API failed: ${officialApiError}, trying fallback approach`);
581
+ logger.log(`Fallback: Creating manual reply draft with enhanced threading`);
582
+ logger.log(`Original message conversation ID: ${originalMessage.conversationId}`);
583
+ logger.log(`Original message folder: ${originalMessage.parentFolderId}`);
584
+ // Build proper References header from existing chain
585
+ let referencesHeader = originalMessage.internetMessageId || originalMessage.id;
586
+ if (originalMessage.internetMessageHeaders) {
587
+ const existingReferences = originalMessage.internetMessageHeaders.find((h) => h.name.toLowerCase() === 'references');
588
+ if (existingReferences && existingReferences.value) {
589
+ referencesHeader = `${existingReferences.value} ${referencesHeader}`;
590
+ }
591
+ }
592
+ const currentUserEmail = await this.getCurrentUserEmail();
593
+ const draftBody = {
594
+ subject: originalMessage.subject?.startsWith('Re:') ? originalMessage.subject : `Re: ${originalMessage.subject}`,
595
+ body: {
596
+ contentType: 'html',
597
+ content: completeReplyBody
598
+ },
599
+ conversationId: originalMessage.conversationId,
600
+ internetMessageHeaders: [
601
+ {
602
+ name: 'X-In-Reply-To',
603
+ value: originalMessage.internetMessageId || originalMessage.id
604
+ },
605
+ {
606
+ name: 'X-References',
607
+ value: referencesHeader
608
+ },
609
+ {
610
+ name: 'X-Thread-Topic',
611
+ value: originalMessage.subject?.replace(/^Re:\s*/i, '') || ''
612
+ }
613
+ ]
614
+ };
615
+ // Include conversation index if available for proper Outlook threading
616
+ if (originalMessage.conversationIndex) {
617
+ draftBody.internetMessageHeaders.push({
618
+ name: 'X-Thread-Index',
619
+ value: originalMessage.conversationIndex
620
+ });
621
+ }
622
+ // Set recipients based on reply type
623
+ if (replyToAll) {
624
+ draftBody.toRecipients = [
625
+ ...(originalMessage.from ? [{ emailAddress: originalMessage.from.emailAddress }] : []),
626
+ ...(originalMessage.toRecipients || []).filter((r) => r.emailAddress.address !== currentUserEmail)
627
+ ];
628
+ draftBody.ccRecipients = originalMessage.ccRecipients || [];
629
+ }
630
+ else {
631
+ draftBody.toRecipients = originalMessage.from ? [{ emailAddress: originalMessage.from.emailAddress }] : [];
632
+ }
633
+ // Create the fallback draft
634
+ const replyDraft = await graphClient
635
+ .api('/me/messages')
636
+ .post(draftBody);
637
+ logger.log(`Fallback reply draft created with ID: ${replyDraft.id}`);
638
+ logger.log(`Draft conversation ID: ${replyDraft.conversationId}`);
639
+ // Try to move the draft to the same folder as the original message for better threading
640
+ if (originalMessage.parentFolderId && originalMessage.parentFolderId !== 'drafts') {
641
+ try {
642
+ await graphClient
643
+ .api(`/me/messages/${replyDraft.id}/move`)
644
+ .post({
645
+ destinationId: originalMessage.parentFolderId
646
+ });
647
+ logger.log(`Draft moved to original message folder: ${originalMessage.parentFolderId}`);
648
+ }
649
+ catch (moveError) {
650
+ logger.log(`Could not move draft to original folder: ${moveError}`);
651
+ // This is not critical, draft will remain in drafts folder
652
+ }
653
+ }
654
+ return {
655
+ id: replyDraft.id,
656
+ subject: replyDraft.subject,
657
+ conversationId: replyDraft.conversationId,
658
+ toRecipients: replyDraft.toRecipients?.map((r) => r.emailAddress.address),
659
+ ccRecipients: replyDraft.ccRecipients?.map((r) => r.emailAddress.address),
660
+ bodyPreview: replyDraft.bodyPreview,
661
+ isDraft: replyDraft.isDraft,
662
+ parentFolderId: originalMessage.parentFolderId
663
+ };
664
+ }
501
665
  }
502
666
  catch (error) {
503
667
  throw new Error(`Error creating reply draft: ${error}`);
@@ -509,23 +673,106 @@ export class MS365Operations {
509
673
  async createForwardDraft(originalMessageId, comment) {
510
674
  try {
511
675
  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
- };
676
+ logger.log(`Creating forward draft for message: ${originalMessageId}`);
677
+ // First, try using the official Microsoft Graph createForward endpoint for proper threading
678
+ try {
679
+ logger.log(`Using official Graph API endpoint: /me/messages/${originalMessageId}/createForward`);
680
+ const forwardDraft = await graphClient
681
+ .api(`/me/messages/${originalMessageId}/createForward`)
682
+ .post({
683
+ message: {
684
+ body: {
685
+ contentType: 'html',
686
+ content: comment || ''
687
+ }
688
+ }
689
+ });
690
+ logger.log(`Forward draft created successfully with ID: ${forwardDraft.id}`);
691
+ logger.log(`Draft conversation ID: ${forwardDraft.conversationId}`);
692
+ logger.log(`Draft appears as threaded forward in conversation`);
693
+ return {
694
+ id: forwardDraft.id,
695
+ subject: forwardDraft.subject,
696
+ conversationId: forwardDraft.conversationId,
697
+ bodyPreview: forwardDraft.bodyPreview,
698
+ isDraft: forwardDraft.isDraft,
699
+ parentFolderId: forwardDraft.parentFolderId
700
+ };
701
+ }
702
+ catch (officialApiError) {
703
+ logger.log(`Official API failed: ${officialApiError}, trying fallback approach`);
704
+ // Fallback to manual creation if the official endpoint fails
705
+ const originalMessage = await graphClient
706
+ .api(`/me/messages/${originalMessageId}`)
707
+ .select('id,subject,from,toRecipients,ccRecipients,conversationId,parentFolderId,internetMessageId,internetMessageHeaders,conversationIndex,body,sentDateTime')
708
+ .get();
709
+ logger.log(`Fallback: Creating manual forward draft with enhanced threading`);
710
+ logger.log(`Original message conversation ID: ${originalMessage.conversationId}`);
711
+ logger.log(`Original message folder: ${originalMessage.parentFolderId}`);
712
+ // Build proper References header from existing chain
713
+ let referencesHeader = originalMessage.internetMessageId || originalMessage.id;
714
+ if (originalMessage.internetMessageHeaders) {
715
+ const existingReferences = originalMessage.internetMessageHeaders.find((h) => h.name.toLowerCase() === 'references');
716
+ if (existingReferences && existingReferences.value) {
717
+ referencesHeader = `${existingReferences.value} ${referencesHeader}`;
718
+ }
719
+ }
720
+ const forwardedBody = `${comment ? comment + '\n\n' : ''}---------- Forwarded message ----------\nFrom: ${originalMessage.from?.emailAddress?.name || originalMessage.from?.emailAddress?.address}\nDate: ${originalMessage.sentDateTime}\nSubject: ${originalMessage.subject}\nTo: ${originalMessage.toRecipients?.map((r) => r.emailAddress.address).join(', ')}\n\n${originalMessage.body?.content || ''}`;
721
+ const draftBody = {
722
+ subject: originalMessage.subject?.startsWith('Fwd:') ? originalMessage.subject : `Fwd: ${originalMessage.subject}`,
723
+ body: {
724
+ contentType: originalMessage.body?.contentType || 'html',
725
+ content: forwardedBody
726
+ },
727
+ conversationId: originalMessage.conversationId,
728
+ internetMessageHeaders: [
729
+ {
730
+ name: 'X-References',
731
+ value: referencesHeader
732
+ },
733
+ {
734
+ name: 'X-Thread-Topic',
735
+ value: originalMessage.subject?.replace(/^(Re:|Fwd?):\s*/i, '') || ''
736
+ }
737
+ ]
738
+ };
739
+ // Include conversation index if available for proper Outlook threading
740
+ if (originalMessage.conversationIndex) {
741
+ draftBody.internetMessageHeaders.push({
742
+ name: 'X-Thread-Index',
743
+ value: originalMessage.conversationIndex
744
+ });
745
+ }
746
+ // Create the fallback draft
747
+ const forwardDraft = await graphClient
748
+ .api('/me/messages')
749
+ .post(draftBody);
750
+ logger.log(`Fallback forward draft created with ID: ${forwardDraft.id}`);
751
+ logger.log(`Draft conversation ID: ${forwardDraft.conversationId}`);
752
+ // Try to move the draft to the same folder as the original message for better threading
753
+ if (originalMessage.parentFolderId && originalMessage.parentFolderId !== 'drafts') {
754
+ try {
755
+ await graphClient
756
+ .api(`/me/messages/${forwardDraft.id}/move`)
757
+ .post({
758
+ destinationId: originalMessage.parentFolderId
759
+ });
760
+ logger.log(`Draft moved to original message folder: ${originalMessage.parentFolderId}`);
761
+ }
762
+ catch (moveError) {
763
+ logger.log(`Could not move draft to original folder: ${moveError}`);
764
+ // This is not critical, draft will remain in drafts folder
765
+ }
766
+ }
767
+ return {
768
+ id: forwardDraft.id,
769
+ subject: forwardDraft.subject,
770
+ conversationId: forwardDraft.conversationId,
771
+ bodyPreview: forwardDraft.bodyPreview,
772
+ isDraft: forwardDraft.isDraft,
773
+ parentFolderId: originalMessage.parentFolderId
774
+ };
775
+ }
529
776
  }
530
777
  catch (error) {
531
778
  throw new Error(`Error creating forward draft: ${error}`);
@@ -648,10 +895,12 @@ export class MS365Operations {
648
895
  async searchEmails(criteria = {}) {
649
896
  return await this.executeWithAuth(async () => {
650
897
  const graphClient = await this.getGraphClient();
898
+ logger.log(`šŸ” Starting email search with criteria:`, JSON.stringify(criteria, null, 2));
651
899
  // Create cache key from criteria
652
900
  const cacheKey = JSON.stringify(criteria);
653
901
  const cachedResults = this.getCachedResults(cacheKey);
654
902
  if (cachedResults) {
903
+ logger.log('šŸ“¦ Returning cached results');
655
904
  return cachedResults;
656
905
  }
657
906
  const maxResults = criteria.maxResults || 50;
@@ -660,8 +909,18 @@ export class MS365Operations {
660
909
  const maxAttempts = 6; // Try multiple strategies
661
910
  logger.log(`šŸ” Starting persistent search with criteria: ${JSON.stringify(criteria)}`);
662
911
  try {
663
- // Strategy 1: Use Microsoft Graph Search API for text-based queries
664
- if (criteria.query && searchAttempts < maxAttempts) {
912
+ // Strategy 1: Use reliable OData filter search first (has proper IDs)
913
+ if (searchAttempts < maxAttempts) {
914
+ searchAttempts++;
915
+ logger.log(`šŸ” Attempt ${searchAttempts}: Using reliable OData filter search first`);
916
+ const filterResults = await this.performFilteredSearch(criteria, maxResults * 2);
917
+ allMessages.push(...filterResults);
918
+ if (allMessages.length > 0) {
919
+ logger.log(`āœ… Found ${allMessages.length} results with OData filter search`);
920
+ }
921
+ }
922
+ // Strategy 2: Use Microsoft Graph Search API for text-based queries (backup)
923
+ if (allMessages.length === 0 && criteria.query && searchAttempts < maxAttempts) {
665
924
  searchAttempts++;
666
925
  logger.log(`šŸ” Attempt ${searchAttempts}: Using Graph Search API for query: "${criteria.query}"`);
667
926
  const searchResults = await this.performGraphSearch(criteria.query, maxResults * 2); // Search for more to increase chances
@@ -670,7 +929,7 @@ export class MS365Operations {
670
929
  logger.log(`āœ… Found ${allMessages.length} results with Graph Search API`);
671
930
  }
672
931
  }
673
- // Strategy 2: Use KQL (Keyword Query Language) for advanced searches
932
+ // Strategy 3: Use KQL (Keyword Query Language) for advanced searches
674
933
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
675
934
  const kqlQuery = this.buildKQLQuery(criteria);
676
935
  if (kqlQuery) {
@@ -683,7 +942,7 @@ export class MS365Operations {
683
942
  }
684
943
  }
685
944
  }
686
- // Strategy 3: Try relaxed KQL search (remove some constraints)
945
+ // Strategy 4: Try relaxed KQL search (remove some constraints)
687
946
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
688
947
  const relaxedKQL = this.buildRelaxedKQLQuery(criteria);
689
948
  if (relaxedKQL && relaxedKQL !== this.buildKQLQuery(criteria)) {
@@ -696,17 +955,12 @@ export class MS365Operations {
696
955
  }
697
956
  }
698
957
  }
699
- // Strategy 4: OData filter search with broader scope
700
- if (allMessages.length === 0 && searchAttempts < maxAttempts) {
701
- searchAttempts++;
702
- logger.log(`šŸ” Attempt ${searchAttempts}: Using OData filter search with broader scope`);
703
- const filterResults = await this.performFilteredSearch(criteria, maxResults * 3); // Even broader search
704
- allMessages.push(...filterResults);
705
- if (allMessages.length > 0) {
706
- logger.log(`āœ… Found ${allMessages.length} results with OData filter search`);
707
- }
958
+ // Strategy 5: If we found results but they have UNKNOWN IDs, try to resolve them
959
+ if (allMessages.length > 0 && allMessages.some(msg => msg.id === 'UNKNOWN')) {
960
+ logger.log(`šŸ” Attempting to resolve UNKNOWN message IDs using direct message queries`);
961
+ allMessages = await this.resolveUnknownMessageIds(allMessages, criteria);
708
962
  }
709
- // Strategy 5: Partial text search across recent emails (expanded scope)
963
+ // Strategy 6: Partial text search across recent emails (expanded scope)
710
964
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
711
965
  searchAttempts++;
712
966
  logger.log(`šŸ” Attempt ${searchAttempts}: Using expanded partial text search`);
@@ -716,7 +970,7 @@ export class MS365Operations {
716
970
  logger.log(`āœ… Found ${allMessages.length} results with partial text search`);
717
971
  }
718
972
  }
719
- // Strategy 6: Fallback to basic search with maximum scope
973
+ // Strategy 7: Fallback to basic search with maximum scope
720
974
  if (allMessages.length === 0 && searchAttempts < maxAttempts) {
721
975
  searchAttempts++;
722
976
  logger.log(`šŸ” Attempt ${searchAttempts}: Using fallback basic search with maximum scope`);
@@ -773,6 +1027,53 @@ export class MS365Operations {
773
1027
  }
774
1028
  }, 'searchEmails');
775
1029
  }
1030
+ /**
1031
+ * Resolve UNKNOWN message IDs by finding messages using alternative search criteria
1032
+ */
1033
+ async resolveUnknownMessageIds(messages, criteria) {
1034
+ try {
1035
+ const graphClient = await this.getGraphClient();
1036
+ const resolvedMessages = [];
1037
+ for (const message of messages) {
1038
+ if (message.id === 'UNKNOWN') {
1039
+ // Try to find this message using subject and sender
1040
+ try {
1041
+ const searchFilter = [];
1042
+ if (message.subject) {
1043
+ searchFilter.push(`subject eq '${message.subject.replace(/'/g, "''")}'`);
1044
+ }
1045
+ if (message.from?.address) {
1046
+ searchFilter.push(`from/emailAddress/address eq '${message.from.address}'`);
1047
+ }
1048
+ if (searchFilter.length > 0) {
1049
+ const result = await graphClient
1050
+ .api('/me/messages')
1051
+ .filter(searchFilter.join(' and '))
1052
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
1053
+ .top(1)
1054
+ .get();
1055
+ if (result.value && result.value.length > 0) {
1056
+ const resolvedMessage = this.mapEmailResult(result.value[0]);
1057
+ logger.log(`āœ… Resolved UNKNOWN ID to: ${resolvedMessage.id}`);
1058
+ resolvedMessages.push(resolvedMessage);
1059
+ continue;
1060
+ }
1061
+ }
1062
+ }
1063
+ catch (resolveError) {
1064
+ logger.log(`āš ļø Could not resolve message ID: ${resolveError}`);
1065
+ }
1066
+ }
1067
+ // If we couldn't resolve it, keep the original message
1068
+ resolvedMessages.push(message);
1069
+ }
1070
+ return resolvedMessages;
1071
+ }
1072
+ catch (error) {
1073
+ logger.log(`āš ļø Error resolving unknown message IDs: ${error}`);
1074
+ return messages; // Return original messages if resolution fails
1075
+ }
1076
+ }
776
1077
  /**
777
1078
  * Use Microsoft Graph Search API for full-text search
778
1079
  */
@@ -792,21 +1093,31 @@ export class MS365Operations {
792
1093
  }
793
1094
  ]
794
1095
  };
1096
+ logger.log(`šŸ” Graph Search request:`, JSON.stringify(searchRequest, null, 2));
795
1097
  const result = await graphClient
796
1098
  .api('/search/query')
797
1099
  .post(searchRequest);
1100
+ logger.log(`šŸ” Graph Search raw response:`, JSON.stringify(result, null, 2));
798
1101
  const messages = [];
799
1102
  if (result.value?.[0]?.hitsContainers?.[0]?.hits) {
1103
+ logger.log(`šŸ” Processing ${result.value[0].hitsContainers[0].hits.length} hits`);
800
1104
  for (const hit of result.value[0].hitsContainers[0].hits) {
801
1105
  const email = hit.resource;
802
- messages.push(this.mapEmailResult(email));
1106
+ logger.log(`šŸ” Raw email from Graph Search:`, JSON.stringify(email, null, 2));
1107
+ const mappedEmail = this.mapEmailResult(email);
1108
+ logger.log(`šŸ” Mapped email:`, JSON.stringify(mappedEmail, null, 2));
1109
+ messages.push(mappedEmail);
803
1110
  }
804
1111
  }
1112
+ else {
1113
+ logger.log(`šŸ” No hits found in Graph Search response structure`);
1114
+ }
805
1115
  logger.log(`šŸ“§ Graph Search returned ${messages.length} results`);
806
1116
  return messages;
807
1117
  }
808
1118
  catch (error) {
809
1119
  logger.log(`āŒ Graph Search failed: ${error}. Falling back to alternative search.`);
1120
+ logger.error(`āŒ Graph Search error details:`, JSON.stringify(error, null, 2));
810
1121
  return [];
811
1122
  }
812
1123
  }
@@ -975,8 +1286,33 @@ export class MS365Operations {
975
1286
  * Map email result to EmailInfo format
976
1287
  */
977
1288
  mapEmailResult(email) {
1289
+ // Extract ID from various possible locations in the response
1290
+ let emailId = email.id;
1291
+ if (!emailId) {
1292
+ // Try alternative ID sources
1293
+ emailId = email['@odata.id'] ||
1294
+ email.internetMessageId ||
1295
+ email._id ||
1296
+ email.messageId ||
1297
+ email.resource?.id;
1298
+ // If still no ID, try to extract from webLink or other fields
1299
+ if (!emailId && email.webLink) {
1300
+ const linkMatch = email.webLink.match(/itemid=([^&]+)/i);
1301
+ if (linkMatch) {
1302
+ emailId = decodeURIComponent(linkMatch[1]);
1303
+ }
1304
+ }
1305
+ // Log the raw email object for debugging when ID is missing
1306
+ if (!emailId) {
1307
+ logger.log('DEBUG: Email object missing ID after all attempts:', JSON.stringify(email, null, 2));
1308
+ emailId = 'UNKNOWN';
1309
+ }
1310
+ else {
1311
+ logger.log(`DEBUG: Found email ID from alternative source: ${emailId}`);
1312
+ }
1313
+ }
978
1314
  return {
979
- id: email.id,
1315
+ id: emailId,
980
1316
  subject: email.subject || '',
981
1317
  from: {
982
1318
  name: email.from?.emailAddress?.name || '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms365-mcp-server",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
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",