@adeu/mcp-server 1.9.0 → 1.10.0

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
@@ -320,7 +320,8 @@ async function login_to_adeu_cloud() {
320
320
  headers: {
321
321
  Authorization: `Bearer ${apiKey}`,
322
322
  Accept: "application/json"
323
- }
323
+ },
324
+ signal: AbortSignal.timeout(15e3)
324
325
  });
325
326
  if (res.status === 401) {
326
327
  DesktopAuthManager.clearApiKey();
@@ -359,8 +360,41 @@ import { homedir as homedir2, tmpdir } from "os";
359
360
  import { join as join2 } from "path";
360
361
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
361
362
  import { createHash } from "crypto";
363
+ var KNOWN_ERROR_HINTS = {
364
+ "Email not found.": "The email ID was not found. If this was a short ID (msg_*), it may have been evicted from the local cache or come from a different machine \u2014 re-run search_and_fetch_emails with filters to get a fresh ID. If it was an adeu_<numeric> or raw provider ID, verify it's correct.",
365
+ "Adeu email reference not found.": "The adeu_<id> reference doesn't resolve to any processed email for this user. Verify the ID, or re-run search_and_fetch_emails with filters to find the message.",
366
+ "Invalid adeu_ email ID format.": "The adeu_<id> reference is malformed. Expected format: adeu_<integer>."
367
+ };
368
+ function formatBackendError(statusCode, responseBody) {
369
+ let detail = responseBody;
370
+ try {
371
+ const parsed = JSON.parse(responseBody);
372
+ if (parsed && typeof parsed === "object" && "detail" in parsed) {
373
+ detail = String(parsed.detail);
374
+ }
375
+ } catch {
376
+ }
377
+ let hint = KNOWN_ERROR_HINTS[detail];
378
+ if (!hint && detail.startsWith("Mailbox '") && detail.endsWith("' not found.")) {
379
+ const mailbox = detail.slice("Mailbox '".length, -"' not found.".length);
380
+ hint = `The mailbox '${mailbox}' is not connected to your Adeu account. Call list_available_mailboxes to see valid mailbox addresses, then retry with one of those as \`mailbox_address\`.`;
381
+ }
382
+ const message = hint ?? detail;
383
+ return `Cloud search failed (HTTP ${statusCode}): ${message}`;
384
+ }
385
+ function isTimeoutError(err) {
386
+ if (!err || typeof err !== "object") return false;
387
+ const name = err.name;
388
+ return name === "TimeoutError" || name === "AbortError";
389
+ }
362
390
  var CACHE_FILE = join2(homedir2(), ".adeu", "mcp_id_cache.json");
363
391
  var MAX_CACHE_SIZE = 1e3;
392
+ function formatBytes(bytes) {
393
+ if (bytes == null) return "unknown size";
394
+ if (bytes < 1024) return `${bytes} B`;
395
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
396
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
397
+ }
364
398
  function loadIdCache() {
365
399
  if (existsSync2(CACHE_FILE)) {
366
400
  try {
@@ -391,32 +425,173 @@ function minifyEmailId(realId, cache) {
391
425
  cache[shortId] = realId;
392
426
  return shortId;
393
427
  }
428
+ var StaleShortIdError = class extends Error {
429
+ constructor(shortId) {
430
+ super(
431
+ `Short ID '${shortId}' is not in the local cache (it may have been evicted, or it came from a different machine/session). Short IDs only persist on the machine where they were generated. Re-run search_and_fetch_emails with filters (sender, subject, days_ago) to fetch fresh IDs, then use the new ID from those results.`
432
+ );
433
+ this.name = "StaleShortIdError";
434
+ }
435
+ };
394
436
  function resolveEmailId(shortId) {
395
437
  if (!shortId) return shortId;
438
+ if (shortId.startsWith("adeu_")) return shortId;
396
439
  const cache = loadIdCache();
397
- return cache[shortId] || shortId;
440
+ const resolved = cache[shortId];
441
+ if (resolved) return resolved;
442
+ if (shortId.startsWith("msg_")) {
443
+ throw new StaleShortIdError(shortId);
444
+ }
445
+ return shortId;
446
+ }
447
+ var HTML_NAMED_ENTITIES = {
448
+ nbsp: " ",
449
+ amp: "&",
450
+ lt: "<",
451
+ gt: ">",
452
+ quot: '"',
453
+ apos: "'",
454
+ copy: "\xA9",
455
+ reg: "\xAE",
456
+ trade: "\u2122",
457
+ hellip: "\u2026",
458
+ mdash: "\u2014",
459
+ ndash: "\u2013",
460
+ lsquo: "\u2018",
461
+ rsquo: "\u2019",
462
+ ldquo: "\u201C",
463
+ rdquo: "\u201D",
464
+ laquo: "\xAB",
465
+ raquo: "\xBB",
466
+ bull: "\u2022",
467
+ middot: "\xB7",
468
+ deg: "\xB0",
469
+ plusmn: "\xB1",
470
+ times: "\xD7",
471
+ divide: "\xF7",
472
+ euro: "\u20AC",
473
+ pound: "\xA3",
474
+ yen: "\xA5",
475
+ cent: "\xA2",
476
+ sect: "\xA7",
477
+ para: "\xB6",
478
+ iexcl: "\xA1",
479
+ iquest: "\xBF"
480
+ };
481
+ function decodeHtmlEntities(text) {
482
+ text = text.replace(/&#(\d+);/g, (_, dec) => {
483
+ const code = parseInt(dec, 10);
484
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
485
+ });
486
+ text = text.replace(/&#[xX]([0-9a-fA-F]+);/g, (_, hex) => {
487
+ const code = parseInt(hex, 16);
488
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
489
+ });
490
+ text = text.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name) => {
491
+ const replacement = HTML_NAMED_ENTITIES[name.toLowerCase()];
492
+ return replacement !== void 0 ? replacement : match;
493
+ });
494
+ return text;
398
495
  }
399
496
  function stripTags(html) {
400
497
  if (!html) return "";
401
- let text = html.replace(/<(style|script|head)[^>]*>[\s\S]*?<\/\1>/gi, "");
498
+ let text = html;
499
+ const suppressPattern = /<(style|script|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
500
+ let prev;
501
+ do {
502
+ prev = text;
503
+ text = text.replace(suppressPattern, "");
504
+ } while (text !== prev);
505
+ text = text.replace(/<(style|script|head|title)\b[^>]*>[\s\S]*$/gi, "");
402
506
  text = text.replace(
403
507
  /<\/?(p|div|br|hr|tr|li|h[1-6]|blockquote)\b[^>]*>/gi,
404
508
  "\n"
405
509
  );
406
510
  text = text.replace(/<[^>]+>/g, "");
511
+ text = decodeHtmlEntities(text);
407
512
  return text.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
408
513
  }
409
514
  function removeNestedQuotes(text) {
410
515
  if (!text) return "";
411
- const patterns = [
516
+ const fromTokens = [
517
+ "From",
518
+ // English
519
+ "L\xE4hett\xE4j\xE4",
520
+ // Finnish
521
+ "Fr\xE5n",
522
+ // Swedish
523
+ "Von",
524
+ // German
525
+ "De",
526
+ // French / Spanish / Portuguese
527
+ "Da",
528
+ // Italian
529
+ "Van",
530
+ // Dutch
531
+ "Fra",
532
+ // Norwegian / Danish
533
+ "Mittente"
534
+ // Italian (alt)
535
+ ];
536
+ const sentTokens = [
537
+ "Sent",
538
+ "L\xE4hetetty",
539
+ "Skickat",
540
+ "Gesendet",
541
+ "Envoy\xE9",
542
+ "Enviado",
543
+ "Inviato",
544
+ "Verzonden",
545
+ "Sendt"
546
+ ];
547
+ const wrotePatterns = [
548
+ /On .{1,200}? wrote:/,
549
+ // English
550
+ /Le .{1,200}? a écrit\s*:/i,
551
+ // French
552
+ /Am .{1,200}? schrieb .{1,100}?:/i,
553
+ // German
554
+ /El .{1,200}? escribió\s*:/i,
555
+ // Spanish
556
+ /Il .{1,200}? ha scritto\s*:/i,
557
+ // Italian
558
+ /Op .{1,200}? schreef .{1,100}?:/i,
559
+ // Dutch
560
+ /Den .{1,200}? skrev .{1,100}?:/i,
561
+ // Swedish/Norwegian/Danish
562
+ /Em .{1,200}? escreveu\s*:/i,
563
+ // Portuguese
564
+ /Em\b.{1,200}?, .{1,200}? escreveu\s*:/i,
565
+ // Portuguese (date prefix)
566
+ new RegExp(
567
+ `^(${fromTokens.join("|")})\\s*:.*?\\n(?:.*\\n){0,5}?(${sentTokens.join("|")})\\s*:`,
568
+ "m"
569
+ )
570
+ ];
571
+ const forwardedTokens = [
572
+ "Forwarded message",
573
+ "V\xE4litetty viesti",
574
+ "Vidarebefordrat meddelande",
575
+ "Weitergeleitete Nachricht",
576
+ "Message transf\xE9r\xE9",
577
+ "Mensaje reenviado",
578
+ "Messaggio inoltrato",
579
+ "Doorgestuurd bericht",
580
+ "Videresendt melding",
581
+ "Videresendt meddelelse",
582
+ "Mensagem encaminhada"
583
+ ].join("|");
584
+ const dividerPatterns = [
412
585
  /_{10,}/m,
413
- /^From:\s.*?\n(?:.*\n){0,5}?Sent:\s/m,
414
- /-----Original Message-----/m,
415
- /On .{1,200}? wrote:/m,
416
- /^Original Message$/m
586
+ /-----\s*(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht|Original meddelande)\s*-----/im,
587
+ /^(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht)$/im,
588
+ // Gmail/Outlook-style "---------- Forwarded message ---------" with localized variants
589
+ new RegExp(`-+\\s*(${forwardedTokens})\\s*-+`, "i"),
590
+ new RegExp(`^(${forwardedTokens})$`, "im")
417
591
  ];
592
+ const allPatterns = [...wrotePatterns, ...dividerPatterns];
418
593
  let earliestCut = text.length;
419
- for (const pattern of patterns) {
594
+ for (const pattern of allPatterns) {
420
595
  const match = pattern.exec(text);
421
596
  if (match && match.index < earliestCut) {
422
597
  earliestCut = match.index;
@@ -425,20 +600,23 @@ function removeNestedQuotes(text) {
425
600
  return text.substring(0, earliestCut).trim();
426
601
  }
427
602
  function getUniqueFilepath(saveDir, filename) {
428
- let filepath = join2(saveDir, filename);
429
- let counter = 1;
430
- const parts = filename.split(".");
431
- const ext = parts.length > 1 ? `.${parts.pop()}` : "";
432
- const stem = parts.join(".");
433
- while (existsSync2(filepath)) {
434
- filepath = join2(saveDir, `${stem}_${counter}${ext}`);
435
- counter++;
436
- }
437
- return filepath;
603
+ return join2(saveDir, filename);
438
604
  }
439
605
  async function search_and_fetch_emails(args) {
440
606
  const apiKey = await getCloudAuthToken();
441
- const realEmailId = args.email_id ? resolveEmailId(args.email_id) : void 0;
607
+ const maxAttachmentSizeMb = typeof args.max_attachment_size_mb === "number" && args.max_attachment_size_mb > 0 ? args.max_attachment_size_mb : 10;
608
+ let realEmailId;
609
+ try {
610
+ realEmailId = args.email_id ? resolveEmailId(args.email_id) : void 0;
611
+ } catch (err) {
612
+ if (err instanceof StaleShortIdError) {
613
+ return {
614
+ isError: true,
615
+ content: [{ type: "text", text: err.message }]
616
+ };
617
+ }
618
+ throw err;
619
+ }
442
620
  const payload = {
443
621
  email_id: realEmailId,
444
622
  sender: args.sender,
@@ -455,21 +633,33 @@ async function search_and_fetch_emails(args) {
455
633
  Object.keys(payload).forEach(
456
634
  (k) => payload[k] === void 0 && delete payload[k]
457
635
  );
458
- const res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
459
- method: "POST",
460
- headers: {
461
- Authorization: `Bearer ${apiKey}`,
462
- "Content-Type": "application/json"
463
- },
464
- body: JSON.stringify(payload)
465
- });
636
+ let res;
637
+ try {
638
+ res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
639
+ method: "POST",
640
+ headers: {
641
+ Authorization: `Bearer ${apiKey}`,
642
+ "Content-Type": "application/json"
643
+ },
644
+ body: JSON.stringify(payload),
645
+ signal: AbortSignal.timeout(45e3)
646
+ });
647
+ } catch (err) {
648
+ if (isTimeoutError(err)) {
649
+ throw new Error(
650
+ "Email search timed out after 45s. The mail provider (Outlook/Gmail) may be slow. Try narrowing the search with more filters (sender, subject, days_ago), or retry shortly."
651
+ );
652
+ }
653
+ throw err;
654
+ }
466
655
  if (res.status === 401) {
467
656
  DesktopAuthManager.clearApiKey();
468
657
  throw new Error(
469
658
  "Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
470
659
  );
471
660
  }
472
- if (!res.ok) throw new Error(`Cloud search failed: ${await res.text()}`);
661
+ if (!res.ok)
662
+ throw new Error(formatBackendError(res.status, await res.text()));
473
663
  const data = await res.json();
474
664
  const cache = loadIdCache();
475
665
  if (data.type === "previews") {
@@ -501,8 +691,12 @@ async function search_and_fetch_emails(args) {
501
691
  );
502
692
  }
503
693
  saveIdCache(cache);
694
+ const limit = typeof args.limit === "number" ? args.limit : 10;
695
+ const offset = typeof args.offset === "number" ? args.offset : 0;
696
+ const pageHint = previews.length >= limit ? `
697
+ *(If you need to see more results, call this tool again with offset=${offset + limit})*` : "";
504
698
  lines.push(
505
- "\u26A0\uFE0F **ACTION REQUIRED**: To read the full body of an email and download its attachments, call this tool again and provide the exact `email_id`."
699
+ "\u26A0\uFE0F **ACTION REQUIRED**: To read the full body of an email and download its attachments, call this tool again and provide the exact `email_id`." + pageHint
506
700
  );
507
701
  return {
508
702
  content: [{ type: "text", text: lines.join("\n") }],
@@ -513,6 +707,7 @@ async function search_and_fetch_emails(args) {
513
707
  const full = data.full_email || {};
514
708
  const shortTargetId = minifyEmailId(full.id || "unknown_id", cache);
515
709
  saveIdCache(cache);
710
+ const autoEscalated = !args.email_id && (args.sender !== void 0 || args.subject !== void 0 || args.has_attachments !== void 0 || args.attachment_name !== void 0 || args.is_unread !== void 0 || args.days_ago !== void 0 || args.folder !== void 0);
516
711
  const baseDir = args.working_directory && existsSync2(args.working_directory) ? args.working_directory : tmpdir();
517
712
  const saveDir = join2(
518
713
  baseDir,
@@ -522,35 +717,67 @@ async function search_and_fetch_emails(args) {
522
717
  mkdirSync2(saveDir, { recursive: true });
523
718
  async function processAttachments(msg) {
524
719
  const localFiles = [];
720
+ const skipped = [];
721
+ const maxBytes = maxAttachmentSizeMb * 1024 * 1024;
525
722
  for (const att of msg.attachments || []) {
723
+ const filename = att.filename || "unnamed_file";
724
+ const size = typeof att.size_bytes === "number" ? att.size_bytes : null;
725
+ if (size != null && size > maxBytes) {
726
+ skipped.push({
727
+ filename,
728
+ size_bytes: size,
729
+ reason: `exceeds ${maxAttachmentSizeMb} MB cap`
730
+ });
731
+ delete att.base64_data;
732
+ continue;
733
+ }
526
734
  if (att.base64_data) {
527
735
  try {
528
- const filepath = getUniqueFilepath(
529
- saveDir,
530
- att.filename || "unnamed_file"
531
- );
736
+ const filepath = getUniqueFilepath(saveDir, filename);
532
737
  writeFileSync2(filepath, Buffer.from(att.base64_data, "base64"));
533
738
  localFiles.push(filepath);
739
+ att.local_path = filepath;
534
740
  delete att.base64_data;
535
741
  } catch (e) {
536
- console.error(`Failed to save attachment ${att.filename}`, e);
742
+ console.error(`Failed to save attachment ${filename}`, e);
743
+ skipped.push({
744
+ filename,
745
+ size_bytes: size,
746
+ reason: `download failed: ${e.message}`
747
+ });
537
748
  }
538
749
  }
539
750
  }
540
- return localFiles;
751
+ return { localFiles, skipped };
541
752
  }
542
- const targetFiles = await processAttachments(full);
543
- const lines = [
753
+ const { localFiles: targetFiles, skipped: targetSkipped } = await processAttachments(full);
754
+ const lines = [];
755
+ if (autoEscalated) {
756
+ lines.push(
757
+ "_(Search returned exactly one result; auto-fetched full email below.)_\n"
758
+ );
759
+ }
760
+ lines.push(
544
761
  `# Email Thread: ${full.subject}`,
545
762
  "",
546
763
  "## Target Message (Newest):",
547
764
  `**From**: ${full.sender_name} <${full.sender_email}>`,
548
765
  `**Date**: ${full.received_datetime}`
549
- ];
766
+ );
550
767
  if (targetFiles.length) {
551
768
  lines.push("**Attachments Saved Locally**:");
552
769
  targetFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
553
770
  }
771
+ if (targetSkipped.length) {
772
+ lines.push(
773
+ `**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` to raise the ${maxAttachmentSizeMb} MB cap:`
774
+ );
775
+ targetSkipped.forEach(
776
+ (s) => lines.push(
777
+ `- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
778
+ )
779
+ );
780
+ }
554
781
  const cleanBody = removeNestedQuotes(stripTags(full.body_html || ""));
555
782
  lines.push(`**Body**:
556
783
  \`\`\`
@@ -561,7 +788,7 @@ ${cleanBody}
561
788
  lines.push("## Previous Messages in Thread (Historical Context):");
562
789
  for (let i = 0; i < full.messages.length; i++) {
563
790
  const histMsg = full.messages[i];
564
- const histFiles = await processAttachments(histMsg);
791
+ const { localFiles: histFiles, skipped: histSkipped } = await processAttachments(histMsg);
565
792
  lines.push(
566
793
  `### Message -${i + 1} (Older)
567
794
  **From**: ${histMsg.sender_name} <${histMsg.sender_email}>
@@ -571,6 +798,16 @@ ${cleanBody}
571
798
  lines.push("**Attachments Saved Locally**:");
572
799
  histFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
573
800
  }
801
+ if (histSkipped.length) {
802
+ lines.push(
803
+ `**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` \u2014 raise the cap:`
804
+ );
805
+ histSkipped.forEach(
806
+ (s) => lines.push(
807
+ `- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
808
+ )
809
+ );
810
+ }
574
811
  lines.push(
575
812
  `**Body**:
576
813
  \`\`\`
@@ -580,6 +817,14 @@ ${removeNestedQuotes(stripTags(histMsg.body_html || ""))}
580
817
  );
581
818
  }
582
819
  }
820
+ const hasAttachments = targetFiles.length > 0 || full.messages && full.messages.some(
821
+ (m) => m.attachments && m.attachments.length > 0
822
+ );
823
+ if (hasAttachments) {
824
+ lines.push(
825
+ "\n*You can now use tools like `read_docx`, `diff_docx_files`, or `finalize_document` on the local file paths listed under each message.*"
826
+ );
827
+ }
583
828
  return {
584
829
  content: [{ type: "text", text: lines.join("\n") }],
585
830
  structuredContent: data
@@ -600,10 +845,20 @@ async function create_email_draft(args) {
600
845
  const formData = new FormData();
601
846
  formData.append("body_markdown", args.body_markdown);
602
847
  if (args.reply_to_email_id) {
603
- formData.append(
604
- "reply_to_email_id",
605
- resolveEmailId(args.reply_to_email_id)
606
- );
848
+ try {
849
+ formData.append(
850
+ "reply_to_email_id",
851
+ resolveEmailId(args.reply_to_email_id)
852
+ );
853
+ } catch (err) {
854
+ if (err instanceof StaleShortIdError) {
855
+ return {
856
+ isError: true,
857
+ content: [{ type: "text", text: err.message }]
858
+ };
859
+ }
860
+ throw err;
861
+ }
607
862
  }
608
863
  if (args.subject) formData.append("subject", args.subject);
609
864
  if (args.mailbox_address) {
@@ -621,11 +876,25 @@ async function create_email_draft(args) {
621
876
  formData.append("files", new Blob([buf]), filename);
622
877
  }
623
878
  }
624
- const res = await fetch(`${BACKEND_URL}/api/v1/emails/drafts/new`, {
625
- method: "POST",
626
- headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
627
- body: formData
628
- });
879
+ let res;
880
+ try {
881
+ res = await fetch(`${BACKEND_URL}/api/v1/emails/drafts/new`, {
882
+ method: "POST",
883
+ headers: {
884
+ Authorization: `Bearer ${apiKey}`,
885
+ Accept: "application/json"
886
+ },
887
+ body: formData,
888
+ signal: AbortSignal.timeout(9e4)
889
+ });
890
+ } catch (err) {
891
+ if (isTimeoutError(err)) {
892
+ throw new Error(
893
+ "Draft creation timed out after 90s. If the draft includes large attachments, try splitting them across multiple drafts or omitting the largest files."
894
+ );
895
+ }
896
+ throw err;
897
+ }
629
898
  if (res.status === 401) {
630
899
  DesktopAuthManager.clearApiKey();
631
900
  throw new Error(
@@ -633,7 +902,7 @@ async function create_email_draft(args) {
633
902
  );
634
903
  }
635
904
  if (!res.ok)
636
- throw new Error(`Cloud draft creation failed: ${await res.text()}`);
905
+ throw new Error(formatBackendError(res.status, await res.text()));
637
906
  const data = await res.json();
638
907
  return {
639
908
  content: [
@@ -646,13 +915,24 @@ async function create_email_draft(args) {
646
915
  }
647
916
  async function list_available_mailboxes() {
648
917
  const apiKey = await getCloudAuthToken();
649
- const res = await fetch(`${BACKEND_URL}/api/v1/users/me/shared-mailboxes`, {
650
- method: "GET",
651
- headers: {
652
- Authorization: `Bearer ${apiKey}`,
653
- Accept: "application/json"
918
+ let res;
919
+ try {
920
+ res = await fetch(`${BACKEND_URL}/api/v1/users/me/shared-mailboxes`, {
921
+ method: "GET",
922
+ headers: {
923
+ Authorization: `Bearer ${apiKey}`,
924
+ Accept: "application/json"
925
+ },
926
+ signal: AbortSignal.timeout(15e3)
927
+ });
928
+ } catch (err) {
929
+ if (isTimeoutError(err)) {
930
+ throw new Error(
931
+ "Listing mailboxes timed out after 15s. The Adeu backend may be temporarily unavailable; retry shortly."
932
+ );
654
933
  }
655
- });
934
+ throw err;
935
+ }
656
936
  if (res.status === 401) {
657
937
  DesktopAuthManager.clearApiKey();
658
938
  throw new Error(
@@ -660,7 +940,7 @@ async function list_available_mailboxes() {
660
940
  );
661
941
  }
662
942
  if (!res.ok) {
663
- throw new Error(`Failed to list available mailboxes: ${await res.text()}`);
943
+ throw new Error(formatBackendError(res.status, await res.text()));
664
944
  }
665
945
  const mailboxes = await res.json();
666
946
  if (!mailboxes.length) {
@@ -673,6 +953,9 @@ async function list_available_mailboxes() {
673
953
  ]
674
954
  };
675
955
  }
956
+ mailboxes.sort(
957
+ (a, b) => (a.email_address ?? "").toLowerCase().localeCompare((b.email_address ?? "").toLowerCase())
958
+ );
676
959
  const lines = [
677
960
  "### Connected Mailboxes",
678
961
  "Below is the list of connected mailboxes you have access to. Use the `email_address` as the `mailbox_address` parameter in other tools to query or draft from a specific mailbox:",
@@ -844,7 +1127,7 @@ registerAppTool(
844
1127
  "search_and_fetch_emails",
845
1128
  {
846
1129
  title: "Search & Fetch Emails",
847
- description: "Searches the user's live email inbox. Returns previews. Call again with `email_id` to fetch the full body.",
1130
+ description: "Searches the user's live email inbox via the Adeu cloud backend.\n\nTWO MODES:\n1. Search mode (no `email_id`): returns up to `limit` lightweight previews. Use filters (`sender`, `subject`, `is_unread`, `days_ago`, `folder`, `has_attachments`, `attachment_name`) to narrow down.\n2. Fetch mode (with `email_id`): returns the full email body, thread history, and downloads attachments under `max_attachment_size_mb` to the local disk.\n\nAUTO-ESCALATION: If a search returns exactly one preview, the backend automatically fetches the full email in the same call. Plan around the response shape \u2014 check the `type` field (`previews` vs `full_email`) before assuming.\n\nEMAIL ID FORMATS (`email_id` parameter accepts any of):\n- `msg_<6 chars>` \u2014 short ID returned by previews on THIS machine. NOT portable across machines or sessions; the local cache holds the most recent 1000. If you reference one that's been evicted, the tool returns a StaleShortIdError telling you to re-search.\n- `adeu_<numeric>` \u2014 server-side reference for emails Adeu has previously processed. Portable across machines and sessions for the same authenticated user.\n- Raw provider ID (Gmail/Outlook native ID) \u2014 works if you have it, but you usually won't.\n\nFOLDER DEFAULT: omitting `folder` searches the Inbox only (matching what the user sees in their mail client). Use `folder='sent'` for sent items, `folder='all'` to include Deleted Items, Drafts, and other folders.\n\nATTACHMENTS: attachments larger than `max_attachment_size_mb` (default 10) are listed in the response but NOT downloaded \u2014 raise the cap if you need them. Always set `working_directory` when calling from a project so attachments land alongside the user's other files.",
848
1131
  inputSchema: z.object({
849
1132
  sender: z.string().optional(),
850
1133
  subject: z.string().optional(),
@@ -857,7 +1140,10 @@ registerAppTool(
857
1140
  offset: z.number().default(0),
858
1141
  email_id: z.string().optional(),
859
1142
  working_directory: z.string().optional(),
860
- mailbox_address: z.string().optional().describe("Optional target mailbox email address to search within.")
1143
+ mailbox_address: z.string().optional().describe("Optional target mailbox email address to search within."),
1144
+ max_attachment_size_mb: z.number().optional().describe(
1145
+ "Maximum attachment size in MB to download (default 10). Attachments larger than this are listed in the response but not downloaded. Raise this to fetch large files."
1146
+ )
861
1147
  }),
862
1148
  _meta: { ui: { resourceUri: EMAIL_UI_URI } }
863
1149
  },
@@ -867,12 +1153,7 @@ registerAppTool(
867
1153
  } catch (e) {
868
1154
  return {
869
1155
  isError: true,
870
- content: [
871
- {
872
- type: "text",
873
- text: `Error executing tool search_and_fetch_emails: ${e.message}`
874
- }
875
- ]
1156
+ content: [{ type: "text", text: e.message }]
876
1157
  };
877
1158
  }
878
1159
  }
@@ -885,10 +1166,11 @@ server.registerTool(
885
1166
  original_docx_path: z.string().describe("Absolute path to the source file."),
886
1167
  author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
887
1168
  changes: z.array(z.any()).describe("List of changes to apply. Each change must specify 'type'."),
888
- output_path: z.string().optional().describe("Optional output path.")
1169
+ output_path: z.string().optional().describe("Optional output path."),
1170
+ dry_run: z.boolean().optional().default(false).describe("If True, simulates the changes and returns a detailed preview report without modifying any files.")
889
1171
  }
890
1172
  },
891
- async ({ original_docx_path, author_name, changes, output_path }) => {
1173
+ async ({ original_docx_path, author_name, changes, output_path, dry_run }) => {
892
1174
  try {
893
1175
  if (!author_name || !author_name.trim())
894
1176
  return {
@@ -912,7 +1194,7 @@ server.registerTool(
912
1194
  const engine = new RedlineEngine(doc, author_name);
913
1195
  let stats;
914
1196
  try {
915
- stats = engine.process_batch(changes);
1197
+ stats = engine.process_batch(changes, dry_run);
916
1198
  } catch (e) {
917
1199
  if (e instanceof BatchValidationError) {
918
1200
  return {
@@ -929,17 +1211,11 @@ ${e.errors.join("\n\n")}`
929
1211
  }
930
1212
  throw e;
931
1213
  }
932
- const outBuf = await doc.save();
933
- fs.writeFileSync(outPath, outBuf);
934
- let res = `Batch complete. Saved to: ${outPath}
935
- Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
936
- Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.`;
937
- if (stats.skipped_details?.length > 0) {
938
- res += `
939
-
940
- Skipped Details:
941
- ${stats.skipped_details.join("\n")}`;
1214
+ if (!dry_run) {
1215
+ const outBuf = await doc.save();
1216
+ fs.writeFileSync(outPath, outBuf);
942
1217
  }
1218
+ const res = formatBatchResult(stats, outPath, !!dry_run);
943
1219
  return { content: [{ type: "text", text: res }] };
944
1220
  } catch (e) {
945
1221
  return {
@@ -1109,7 +1385,7 @@ server.registerTool(
1109
1385
  server.registerTool(
1110
1386
  "create_email_draft",
1111
1387
  {
1112
- description: "Creates an email draft in the user's native draft box.",
1388
+ description: "Creates an email draft in the user's native draft box (Outlook Drafts or Gmail Drafts).\n\nTWO MODES:\n1. Reply mode: pass `reply_to_email_id` to create a threaded reply. The draft inherits subject, recipients, and threading headers from the original \u2014 do NOT pass `subject` or `to_recipients`.\n2. New email mode: omit `reply_to_email_id` and pass BOTH `subject` and `to_recipients`.\n\n`reply_to_email_id` accepts the same ID formats as search_and_fetch_emails (`msg_*` short IDs, `adeu_*` references, or raw provider IDs). Short IDs are validated against the local cache before the call; stale ones fail fast with a clear error telling you to re-search.\n\n`body_markdown` is converted server-side to styled HTML with inlined CSS for email-client compatibility. Write the body in plain Markdown \u2014 do not pre-render HTML.\n\n`attachment_paths` takes absolute file paths on the user's local disk and uploads them with the draft. Useful right after search_and_fetch_emails downloaded attachments \u2014 those local paths can be passed directly here.",
1113
1389
  inputSchema: {
1114
1390
  body_markdown: z.string(),
1115
1391
  reply_to_email_id: z.string().optional(),
@@ -1132,7 +1408,7 @@ server.registerTool(
1132
1408
  server.registerTool(
1133
1409
  "list_available_mailboxes",
1134
1410
  {
1135
- description: "Lists all personal and shared delegated mailboxes configured for the authenticated profile. Use this to discover valid email addresses to scope search and draft operations.",
1411
+ description: "Lists all personal and shared delegated mailboxes the authenticated user has access to. Returns each mailbox's `email_address`, `display_name`, auto-processing settings, and write-back preference.\n\nCall this FIRST when the user mentions a specific mailbox or shared inbox by name, to resolve the canonical `email_address`. Then pass that address as `mailbox_address` to `search_and_fetch_emails` or `create_email_draft` to scope the operation.\n\nOmitting `mailbox_address` on those tools targets the user's primary personal mailbox.",
1136
1412
  inputSchema: {}
1137
1413
  },
1138
1414
  async () => {
@@ -1143,6 +1419,56 @@ server.registerTool(
1143
1419
  }
1144
1420
  }
1145
1421
  );
1422
+ function formatBatchResult(stats, outPath, dry_run) {
1423
+ let res = "";
1424
+ if (dry_run) {
1425
+ res = `Dry-run simulation complete.
1426
+ `;
1427
+ } else {
1428
+ res = `Batch complete. Saved to: ${outPath}
1429
+ `;
1430
+ }
1431
+ res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
1432
+ `;
1433
+ res += `Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.
1434
+ `;
1435
+ if (stats.edits && stats.edits.length > 0) {
1436
+ res += "\nDetailed Edit Reports:\n";
1437
+ for (let i = 0; i < stats.edits.length; i++) {
1438
+ const report = stats.edits[i];
1439
+ const status_indicator = report.status === "applied" ? "\u2705 [applied]" : "\u274C [failed]";
1440
+ res += `Edit ${i + 1} ${status_indicator}:
1441
+ `;
1442
+ res += ` Target: '${report.target_text}'
1443
+ `;
1444
+ res += ` New text: '${report.new_text}'
1445
+ `;
1446
+ if (report.warning) {
1447
+ res += ` Warning: ${report.warning}
1448
+ `;
1449
+ }
1450
+ if (report.error) {
1451
+ res += ` Error: ${report.error}
1452
+ `;
1453
+ }
1454
+ if (report.critic_markup) {
1455
+ res += ` Preview (CriticMarkup): ${report.critic_markup}
1456
+ `;
1457
+ }
1458
+ if (report.clean_text) {
1459
+ res += ` Clean text preview: ${report.clean_text}
1460
+ `;
1461
+ }
1462
+ }
1463
+ }
1464
+ if (stats.skipped_details && stats.skipped_details.length > 0) {
1465
+ res += `
1466
+
1467
+ Skipped Details:
1468
+ ${stats.skipped_details.join("\n")}`;
1469
+ }
1470
+ return res;
1471
+ }
1146
1472
  async function main() {
1147
1473
  const transport = new StdioServerTransport();
1148
1474
  await server.connect(transport);
@@ -1151,4 +1477,7 @@ async function main() {
1151
1477
  );
1152
1478
  }
1153
1479
  main().catch(console.error);
1480
+ export {
1481
+ formatBatchResult
1482
+ };
1154
1483
  //# sourceMappingURL=index.js.map