@adeu/mcp-server 1.9.0 → 1.10.1

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();
@@ -330,11 +331,14 @@ async function login_to_adeu_cloud() {
330
331
  }
331
332
  if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
332
333
  const data = await res.json();
334
+ const email = data.email || "Unknown Email";
333
335
  return {
334
336
  content: [
335
337
  {
336
338
  type: "text",
337
- text: `Login successful! Connected to Adeu Cloud as: ${data.email || "Unknown Email"}.`
339
+ text: `Login successful. You are now authenticated to Adeu Cloud as the user who owns the provider account \`${email}\` (the account used for SSO).
340
+
341
+ This single login grants access to ALL of this user's linked provider accounts and ALL of their mailboxes for the duration of this session \u2014 not just \`${email}\`. Call \`list_available_mailboxes\` to see every mailbox that can be queried or drafted from.`
338
342
  }
339
343
  ]
340
344
  };
@@ -359,8 +363,41 @@ import { homedir as homedir2, tmpdir } from "os";
359
363
  import { join as join2 } from "path";
360
364
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
361
365
  import { createHash } from "crypto";
366
+ var KNOWN_ERROR_HINTS = {
367
+ "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.",
368
+ "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.",
369
+ "Invalid adeu_ email ID format.": "The adeu_<id> reference is malformed. Expected format: adeu_<integer>."
370
+ };
371
+ function formatBackendError(statusCode, responseBody) {
372
+ let detail = responseBody;
373
+ try {
374
+ const parsed = JSON.parse(responseBody);
375
+ if (parsed && typeof parsed === "object" && "detail" in parsed) {
376
+ detail = String(parsed.detail);
377
+ }
378
+ } catch {
379
+ }
380
+ let hint = KNOWN_ERROR_HINTS[detail];
381
+ if (!hint && detail.startsWith("Mailbox '") && detail.endsWith("' not found.")) {
382
+ const mailbox = detail.slice("Mailbox '".length, -"' not found.".length);
383
+ 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\`.`;
384
+ }
385
+ const message = hint ?? detail;
386
+ return `Cloud search failed (HTTP ${statusCode}): ${message}`;
387
+ }
388
+ function isTimeoutError(err) {
389
+ if (!err || typeof err !== "object") return false;
390
+ const name = err.name;
391
+ return name === "TimeoutError" || name === "AbortError";
392
+ }
362
393
  var CACHE_FILE = join2(homedir2(), ".adeu", "mcp_id_cache.json");
363
394
  var MAX_CACHE_SIZE = 1e3;
395
+ function formatBytes(bytes) {
396
+ if (bytes == null) return "unknown size";
397
+ if (bytes < 1024) return `${bytes} B`;
398
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
399
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
400
+ }
364
401
  function loadIdCache() {
365
402
  if (existsSync2(CACHE_FILE)) {
366
403
  try {
@@ -391,32 +428,173 @@ function minifyEmailId(realId, cache) {
391
428
  cache[shortId] = realId;
392
429
  return shortId;
393
430
  }
431
+ var StaleShortIdError = class extends Error {
432
+ constructor(shortId) {
433
+ super(
434
+ `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.`
435
+ );
436
+ this.name = "StaleShortIdError";
437
+ }
438
+ };
394
439
  function resolveEmailId(shortId) {
395
440
  if (!shortId) return shortId;
441
+ if (shortId.startsWith("adeu_")) return shortId;
396
442
  const cache = loadIdCache();
397
- return cache[shortId] || shortId;
443
+ const resolved = cache[shortId];
444
+ if (resolved) return resolved;
445
+ if (shortId.startsWith("msg_")) {
446
+ throw new StaleShortIdError(shortId);
447
+ }
448
+ return shortId;
449
+ }
450
+ var HTML_NAMED_ENTITIES = {
451
+ nbsp: " ",
452
+ amp: "&",
453
+ lt: "<",
454
+ gt: ">",
455
+ quot: '"',
456
+ apos: "'",
457
+ copy: "\xA9",
458
+ reg: "\xAE",
459
+ trade: "\u2122",
460
+ hellip: "\u2026",
461
+ mdash: "\u2014",
462
+ ndash: "\u2013",
463
+ lsquo: "\u2018",
464
+ rsquo: "\u2019",
465
+ ldquo: "\u201C",
466
+ rdquo: "\u201D",
467
+ laquo: "\xAB",
468
+ raquo: "\xBB",
469
+ bull: "\u2022",
470
+ middot: "\xB7",
471
+ deg: "\xB0",
472
+ plusmn: "\xB1",
473
+ times: "\xD7",
474
+ divide: "\xF7",
475
+ euro: "\u20AC",
476
+ pound: "\xA3",
477
+ yen: "\xA5",
478
+ cent: "\xA2",
479
+ sect: "\xA7",
480
+ para: "\xB6",
481
+ iexcl: "\xA1",
482
+ iquest: "\xBF"
483
+ };
484
+ function decodeHtmlEntities(text) {
485
+ text = text.replace(/&#(\d+);/g, (_, dec) => {
486
+ const code = parseInt(dec, 10);
487
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
488
+ });
489
+ text = text.replace(/&#[xX]([0-9a-fA-F]+);/g, (_, hex) => {
490
+ const code = parseInt(hex, 16);
491
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
492
+ });
493
+ text = text.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name) => {
494
+ const replacement = HTML_NAMED_ENTITIES[name.toLowerCase()];
495
+ return replacement !== void 0 ? replacement : match;
496
+ });
497
+ return text;
398
498
  }
399
499
  function stripTags(html) {
400
500
  if (!html) return "";
401
- let text = html.replace(/<(style|script|head)[^>]*>[\s\S]*?<\/\1>/gi, "");
501
+ let text = html;
502
+ const suppressPattern = /<(style|script|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
503
+ let prev;
504
+ do {
505
+ prev = text;
506
+ text = text.replace(suppressPattern, "");
507
+ } while (text !== prev);
508
+ text = text.replace(/<(style|script|head|title)\b[^>]*>[\s\S]*$/gi, "");
402
509
  text = text.replace(
403
510
  /<\/?(p|div|br|hr|tr|li|h[1-6]|blockquote)\b[^>]*>/gi,
404
511
  "\n"
405
512
  );
406
513
  text = text.replace(/<[^>]+>/g, "");
514
+ text = decodeHtmlEntities(text);
407
515
  return text.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
408
516
  }
409
517
  function removeNestedQuotes(text) {
410
518
  if (!text) return "";
411
- const patterns = [
519
+ const fromTokens = [
520
+ "From",
521
+ // English
522
+ "L\xE4hett\xE4j\xE4",
523
+ // Finnish
524
+ "Fr\xE5n",
525
+ // Swedish
526
+ "Von",
527
+ // German
528
+ "De",
529
+ // French / Spanish / Portuguese
530
+ "Da",
531
+ // Italian
532
+ "Van",
533
+ // Dutch
534
+ "Fra",
535
+ // Norwegian / Danish
536
+ "Mittente"
537
+ // Italian (alt)
538
+ ];
539
+ const sentTokens = [
540
+ "Sent",
541
+ "L\xE4hetetty",
542
+ "Skickat",
543
+ "Gesendet",
544
+ "Envoy\xE9",
545
+ "Enviado",
546
+ "Inviato",
547
+ "Verzonden",
548
+ "Sendt"
549
+ ];
550
+ const wrotePatterns = [
551
+ /On .{1,200}? wrote:/,
552
+ // English
553
+ /Le .{1,200}? a écrit\s*:/i,
554
+ // French
555
+ /Am .{1,200}? schrieb .{1,100}?:/i,
556
+ // German
557
+ /El .{1,200}? escribió\s*:/i,
558
+ // Spanish
559
+ /Il .{1,200}? ha scritto\s*:/i,
560
+ // Italian
561
+ /Op .{1,200}? schreef .{1,100}?:/i,
562
+ // Dutch
563
+ /Den .{1,200}? skrev .{1,100}?:/i,
564
+ // Swedish/Norwegian/Danish
565
+ /Em .{1,200}? escreveu\s*:/i,
566
+ // Portuguese
567
+ /Em\b.{1,200}?, .{1,200}? escreveu\s*:/i,
568
+ // Portuguese (date prefix)
569
+ new RegExp(
570
+ `^(${fromTokens.join("|")})\\s*:.*?\\n(?:.*\\n){0,5}?(${sentTokens.join("|")})\\s*:`,
571
+ "m"
572
+ )
573
+ ];
574
+ const forwardedTokens = [
575
+ "Forwarded message",
576
+ "V\xE4litetty viesti",
577
+ "Vidarebefordrat meddelande",
578
+ "Weitergeleitete Nachricht",
579
+ "Message transf\xE9r\xE9",
580
+ "Mensaje reenviado",
581
+ "Messaggio inoltrato",
582
+ "Doorgestuurd bericht",
583
+ "Videresendt melding",
584
+ "Videresendt meddelelse",
585
+ "Mensagem encaminhada"
586
+ ].join("|");
587
+ const dividerPatterns = [
412
588
  /_{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
589
+ /-----\s*(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht|Original meddelande)\s*-----/im,
590
+ /^(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht)$/im,
591
+ // Gmail/Outlook-style "---------- Forwarded message ---------" with localized variants
592
+ new RegExp(`-+\\s*(${forwardedTokens})\\s*-+`, "i"),
593
+ new RegExp(`^(${forwardedTokens})$`, "im")
417
594
  ];
595
+ const allPatterns = [...wrotePatterns, ...dividerPatterns];
418
596
  let earliestCut = text.length;
419
- for (const pattern of patterns) {
597
+ for (const pattern of allPatterns) {
420
598
  const match = pattern.exec(text);
421
599
  if (match && match.index < earliestCut) {
422
600
  earliestCut = match.index;
@@ -425,20 +603,23 @@ function removeNestedQuotes(text) {
425
603
  return text.substring(0, earliestCut).trim();
426
604
  }
427
605
  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;
606
+ return join2(saveDir, filename);
438
607
  }
439
608
  async function search_and_fetch_emails(args) {
440
609
  const apiKey = await getCloudAuthToken();
441
- const realEmailId = args.email_id ? resolveEmailId(args.email_id) : void 0;
610
+ const maxAttachmentSizeMb = typeof args.max_attachment_size_mb === "number" && args.max_attachment_size_mb > 0 ? args.max_attachment_size_mb : 10;
611
+ let realEmailId;
612
+ try {
613
+ realEmailId = args.email_id ? resolveEmailId(args.email_id) : void 0;
614
+ } catch (err) {
615
+ if (err instanceof StaleShortIdError) {
616
+ return {
617
+ isError: true,
618
+ content: [{ type: "text", text: err.message }]
619
+ };
620
+ }
621
+ throw err;
622
+ }
442
623
  const payload = {
443
624
  email_id: realEmailId,
444
625
  sender: args.sender,
@@ -455,21 +636,33 @@ async function search_and_fetch_emails(args) {
455
636
  Object.keys(payload).forEach(
456
637
  (k) => payload[k] === void 0 && delete payload[k]
457
638
  );
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
- });
639
+ let res;
640
+ try {
641
+ res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
642
+ method: "POST",
643
+ headers: {
644
+ Authorization: `Bearer ${apiKey}`,
645
+ "Content-Type": "application/json"
646
+ },
647
+ body: JSON.stringify(payload),
648
+ signal: AbortSignal.timeout(45e3)
649
+ });
650
+ } catch (err) {
651
+ if (isTimeoutError(err)) {
652
+ throw new Error(
653
+ "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."
654
+ );
655
+ }
656
+ throw err;
657
+ }
466
658
  if (res.status === 401) {
467
659
  DesktopAuthManager.clearApiKey();
468
660
  throw new Error(
469
661
  "Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
470
662
  );
471
663
  }
472
- if (!res.ok) throw new Error(`Cloud search failed: ${await res.text()}`);
664
+ if (!res.ok)
665
+ throw new Error(formatBackendError(res.status, await res.text()));
473
666
  const data = await res.json();
474
667
  const cache = loadIdCache();
475
668
  if (data.type === "previews") {
@@ -501,8 +694,12 @@ async function search_and_fetch_emails(args) {
501
694
  );
502
695
  }
503
696
  saveIdCache(cache);
697
+ const limit = typeof args.limit === "number" ? args.limit : 10;
698
+ const offset = typeof args.offset === "number" ? args.offset : 0;
699
+ const pageHint = previews.length >= limit ? `
700
+ *(If you need to see more results, call this tool again with offset=${offset + limit})*` : "";
504
701
  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`."
702
+ "\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
703
  );
507
704
  return {
508
705
  content: [{ type: "text", text: lines.join("\n") }],
@@ -513,6 +710,7 @@ async function search_and_fetch_emails(args) {
513
710
  const full = data.full_email || {};
514
711
  const shortTargetId = minifyEmailId(full.id || "unknown_id", cache);
515
712
  saveIdCache(cache);
713
+ 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
714
  const baseDir = args.working_directory && existsSync2(args.working_directory) ? args.working_directory : tmpdir();
517
715
  const saveDir = join2(
518
716
  baseDir,
@@ -522,35 +720,67 @@ async function search_and_fetch_emails(args) {
522
720
  mkdirSync2(saveDir, { recursive: true });
523
721
  async function processAttachments(msg) {
524
722
  const localFiles = [];
723
+ const skipped = [];
724
+ const maxBytes = maxAttachmentSizeMb * 1024 * 1024;
525
725
  for (const att of msg.attachments || []) {
726
+ const filename = att.filename || "unnamed_file";
727
+ const size = typeof att.size_bytes === "number" ? att.size_bytes : null;
728
+ if (size != null && size > maxBytes) {
729
+ skipped.push({
730
+ filename,
731
+ size_bytes: size,
732
+ reason: `exceeds ${maxAttachmentSizeMb} MB cap`
733
+ });
734
+ delete att.base64_data;
735
+ continue;
736
+ }
526
737
  if (att.base64_data) {
527
738
  try {
528
- const filepath = getUniqueFilepath(
529
- saveDir,
530
- att.filename || "unnamed_file"
531
- );
739
+ const filepath = getUniqueFilepath(saveDir, filename);
532
740
  writeFileSync2(filepath, Buffer.from(att.base64_data, "base64"));
533
741
  localFiles.push(filepath);
742
+ att.local_path = filepath;
534
743
  delete att.base64_data;
535
744
  } catch (e) {
536
- console.error(`Failed to save attachment ${att.filename}`, e);
745
+ console.error(`Failed to save attachment ${filename}`, e);
746
+ skipped.push({
747
+ filename,
748
+ size_bytes: size,
749
+ reason: `download failed: ${e.message}`
750
+ });
537
751
  }
538
752
  }
539
753
  }
540
- return localFiles;
754
+ return { localFiles, skipped };
541
755
  }
542
- const targetFiles = await processAttachments(full);
543
- const lines = [
756
+ const { localFiles: targetFiles, skipped: targetSkipped } = await processAttachments(full);
757
+ const lines = [];
758
+ if (autoEscalated) {
759
+ lines.push(
760
+ "_(Search returned exactly one result; auto-fetched full email below.)_\n"
761
+ );
762
+ }
763
+ lines.push(
544
764
  `# Email Thread: ${full.subject}`,
545
765
  "",
546
766
  "## Target Message (Newest):",
547
767
  `**From**: ${full.sender_name} <${full.sender_email}>`,
548
768
  `**Date**: ${full.received_datetime}`
549
- ];
769
+ );
550
770
  if (targetFiles.length) {
551
771
  lines.push("**Attachments Saved Locally**:");
552
772
  targetFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
553
773
  }
774
+ if (targetSkipped.length) {
775
+ lines.push(
776
+ `**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` to raise the ${maxAttachmentSizeMb} MB cap:`
777
+ );
778
+ targetSkipped.forEach(
779
+ (s) => lines.push(
780
+ `- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
781
+ )
782
+ );
783
+ }
554
784
  const cleanBody = removeNestedQuotes(stripTags(full.body_html || ""));
555
785
  lines.push(`**Body**:
556
786
  \`\`\`
@@ -561,7 +791,7 @@ ${cleanBody}
561
791
  lines.push("## Previous Messages in Thread (Historical Context):");
562
792
  for (let i = 0; i < full.messages.length; i++) {
563
793
  const histMsg = full.messages[i];
564
- const histFiles = await processAttachments(histMsg);
794
+ const { localFiles: histFiles, skipped: histSkipped } = await processAttachments(histMsg);
565
795
  lines.push(
566
796
  `### Message -${i + 1} (Older)
567
797
  **From**: ${histMsg.sender_name} <${histMsg.sender_email}>
@@ -571,6 +801,16 @@ ${cleanBody}
571
801
  lines.push("**Attachments Saved Locally**:");
572
802
  histFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
573
803
  }
804
+ if (histSkipped.length) {
805
+ lines.push(
806
+ `**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` \u2014 raise the cap:`
807
+ );
808
+ histSkipped.forEach(
809
+ (s) => lines.push(
810
+ `- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
811
+ )
812
+ );
813
+ }
574
814
  lines.push(
575
815
  `**Body**:
576
816
  \`\`\`
@@ -580,6 +820,14 @@ ${removeNestedQuotes(stripTags(histMsg.body_html || ""))}
580
820
  );
581
821
  }
582
822
  }
823
+ const hasAttachments = targetFiles.length > 0 || full.messages && full.messages.some(
824
+ (m) => m.attachments && m.attachments.length > 0
825
+ );
826
+ if (hasAttachments) {
827
+ lines.push(
828
+ "\n*You can now use tools like `read_docx`, `diff_docx_files`, or `finalize_document` on the local file paths listed under each message.*"
829
+ );
830
+ }
583
831
  return {
584
832
  content: [{ type: "text", text: lines.join("\n") }],
585
833
  structuredContent: data
@@ -600,10 +848,20 @@ async function create_email_draft(args) {
600
848
  const formData = new FormData();
601
849
  formData.append("body_markdown", args.body_markdown);
602
850
  if (args.reply_to_email_id) {
603
- formData.append(
604
- "reply_to_email_id",
605
- resolveEmailId(args.reply_to_email_id)
606
- );
851
+ try {
852
+ formData.append(
853
+ "reply_to_email_id",
854
+ resolveEmailId(args.reply_to_email_id)
855
+ );
856
+ } catch (err) {
857
+ if (err instanceof StaleShortIdError) {
858
+ return {
859
+ isError: true,
860
+ content: [{ type: "text", text: err.message }]
861
+ };
862
+ }
863
+ throw err;
864
+ }
607
865
  }
608
866
  if (args.subject) formData.append("subject", args.subject);
609
867
  if (args.mailbox_address) {
@@ -621,11 +879,25 @@ async function create_email_draft(args) {
621
879
  formData.append("files", new Blob([buf]), filename);
622
880
  }
623
881
  }
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
- });
882
+ let res;
883
+ try {
884
+ res = await fetch(`${BACKEND_URL}/api/v1/emails/drafts/new`, {
885
+ method: "POST",
886
+ headers: {
887
+ Authorization: `Bearer ${apiKey}`,
888
+ Accept: "application/json"
889
+ },
890
+ body: formData,
891
+ signal: AbortSignal.timeout(9e4)
892
+ });
893
+ } catch (err) {
894
+ if (isTimeoutError(err)) {
895
+ throw new Error(
896
+ "Draft creation timed out after 90s. If the draft includes large attachments, try splitting them across multiple drafts or omitting the largest files."
897
+ );
898
+ }
899
+ throw err;
900
+ }
629
901
  if (res.status === 401) {
630
902
  DesktopAuthManager.clearApiKey();
631
903
  throw new Error(
@@ -633,7 +905,7 @@ async function create_email_draft(args) {
633
905
  );
634
906
  }
635
907
  if (!res.ok)
636
- throw new Error(`Cloud draft creation failed: ${await res.text()}`);
908
+ throw new Error(formatBackendError(res.status, await res.text()));
637
909
  const data = await res.json();
638
910
  return {
639
911
  content: [
@@ -646,13 +918,24 @@ async function create_email_draft(args) {
646
918
  }
647
919
  async function list_available_mailboxes() {
648
920
  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"
921
+ let res;
922
+ try {
923
+ res = await fetch(`${BACKEND_URL}/api/v1/users/me/shared-mailboxes`, {
924
+ method: "GET",
925
+ headers: {
926
+ Authorization: `Bearer ${apiKey}`,
927
+ Accept: "application/json"
928
+ },
929
+ signal: AbortSignal.timeout(15e3)
930
+ });
931
+ } catch (err) {
932
+ if (isTimeoutError(err)) {
933
+ throw new Error(
934
+ "Listing mailboxes timed out after 15s. The Adeu backend may be temporarily unavailable; retry shortly."
935
+ );
654
936
  }
655
- });
937
+ throw err;
938
+ }
656
939
  if (res.status === 401) {
657
940
  DesktopAuthManager.clearApiKey();
658
941
  throw new Error(
@@ -660,7 +943,7 @@ async function list_available_mailboxes() {
660
943
  );
661
944
  }
662
945
  if (!res.ok) {
663
- throw new Error(`Failed to list available mailboxes: ${await res.text()}`);
946
+ throw new Error(formatBackendError(res.status, await res.text()));
664
947
  }
665
948
  const mailboxes = await res.json();
666
949
  if (!mailboxes.length) {
@@ -673,6 +956,9 @@ async function list_available_mailboxes() {
673
956
  ]
674
957
  };
675
958
  }
959
+ mailboxes.sort(
960
+ (a, b) => (a.email_address ?? "").toLowerCase().localeCompare((b.email_address ?? "").toLowerCase())
961
+ );
676
962
  const lines = [
677
963
  "### Connected Mailboxes",
678
964
  "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 +1130,7 @@ registerAppTool(
844
1130
  "search_and_fetch_emails",
845
1131
  {
846
1132
  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.",
1133
+ 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
1134
  inputSchema: z.object({
849
1135
  sender: z.string().optional(),
850
1136
  subject: z.string().optional(),
@@ -857,7 +1143,10 @@ registerAppTool(
857
1143
  offset: z.number().default(0),
858
1144
  email_id: z.string().optional(),
859
1145
  working_directory: z.string().optional(),
860
- mailbox_address: z.string().optional().describe("Optional target mailbox email address to search within.")
1146
+ mailbox_address: z.string().optional().describe("Optional target mailbox email address to search within."),
1147
+ max_attachment_size_mb: z.number().optional().describe(
1148
+ "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."
1149
+ )
861
1150
  }),
862
1151
  _meta: { ui: { resourceUri: EMAIL_UI_URI } }
863
1152
  },
@@ -867,12 +1156,7 @@ registerAppTool(
867
1156
  } catch (e) {
868
1157
  return {
869
1158
  isError: true,
870
- content: [
871
- {
872
- type: "text",
873
- text: `Error executing tool search_and_fetch_emails: ${e.message}`
874
- }
875
- ]
1159
+ content: [{ type: "text", text: e.message }]
876
1160
  };
877
1161
  }
878
1162
  }
@@ -885,10 +1169,19 @@ server.registerTool(
885
1169
  original_docx_path: z.string().describe("Absolute path to the source file."),
886
1170
  author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
887
1171
  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.")
1172
+ output_path: z.string().optional().describe("Optional output path."),
1173
+ dry_run: z.boolean().optional().default(false).describe(
1174
+ "If True, simulates the changes and returns a detailed preview report without modifying any files."
1175
+ )
889
1176
  }
890
1177
  },
891
- async ({ original_docx_path, author_name, changes, output_path }) => {
1178
+ async ({
1179
+ original_docx_path,
1180
+ author_name,
1181
+ changes,
1182
+ output_path,
1183
+ dry_run
1184
+ }) => {
892
1185
  try {
893
1186
  if (!author_name || !author_name.trim())
894
1187
  return {
@@ -912,7 +1205,7 @@ server.registerTool(
912
1205
  const engine = new RedlineEngine(doc, author_name);
913
1206
  let stats;
914
1207
  try {
915
- stats = engine.process_batch(changes);
1208
+ stats = engine.process_batch(changes, dry_run);
916
1209
  } catch (e) {
917
1210
  if (e instanceof BatchValidationError) {
918
1211
  return {
@@ -929,17 +1222,11 @@ ${e.errors.join("\n\n")}`
929
1222
  }
930
1223
  throw e;
931
1224
  }
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")}`;
1225
+ if (!dry_run) {
1226
+ const outBuf = await doc.save();
1227
+ fs.writeFileSync(outPath, outBuf);
942
1228
  }
1229
+ const res = formatBatchResult(stats, outPath, !!dry_run);
943
1230
  return { content: [{ type: "text", text: res }] };
944
1231
  } catch (e) {
945
1232
  return {
@@ -1086,7 +1373,9 @@ ${result.reportText}`
1086
1373
  );
1087
1374
  server.registerTool(
1088
1375
  "login_to_adeu_cloud",
1089
- { description: "Logs the user into the Adeu Cloud backend." },
1376
+ {
1377
+ description: "Logs the user into Adeu Cloud. Opens a browser window for SSO authentication.\n\nIMPORTANT \u2014 login is user-level, not account-level:\n- An Adeu user can have multiple linked provider accounts (Microsoft, Google) and multiple mailboxes (personal + shared/delegated). One linked account is marked primary.\n- Signing in through ANY of the user's linked accounts authenticates the same Adeu user. Once logged in, the session can read from and draft in ALL of that user's linked accounts and ALL of their mailboxes \u2014 not just the one used to sign in.\n- The choice of which provider account to sign in through is purely an SSO mechanism; it does not select a 'current account' for the session.\n\nWhen the user asks which accounts or mailboxes are available, call `list_available_mailboxes` rather than naming a single account from the login response."
1378
+ },
1090
1379
  async () => {
1091
1380
  try {
1092
1381
  return await login_to_adeu_cloud();
@@ -1109,7 +1398,7 @@ server.registerTool(
1109
1398
  server.registerTool(
1110
1399
  "create_email_draft",
1111
1400
  {
1112
- description: "Creates an email draft in the user's native draft box.",
1401
+ 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
1402
  inputSchema: {
1114
1403
  body_markdown: z.string(),
1115
1404
  reply_to_email_id: z.string().optional(),
@@ -1132,7 +1421,7 @@ server.registerTool(
1132
1421
  server.registerTool(
1133
1422
  "list_available_mailboxes",
1134
1423
  {
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.",
1424
+ description: "Lists all personal and shared/delegated mailboxes the authenticated Adeu user has access to, across ALL of their linked provider accounts. Returns each mailbox's `email_address`, `display_name`, auto-processing settings, and write-back preference.\n\nThis is the right tool to answer 'which accounts/mailboxes am I logged into?' \u2014 Adeu login is user-level, so a single MCP session can see every mailbox listed here regardless of which provider account was used for SSO.\n\nCall this FIRST when the user names a specific mailbox or shared inbox, 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. Omitting `mailbox_address` on those tools targets the user's primary personal mailbox.",
1136
1425
  inputSchema: {}
1137
1426
  },
1138
1427
  async () => {
@@ -1143,6 +1432,56 @@ server.registerTool(
1143
1432
  }
1144
1433
  }
1145
1434
  );
1435
+ function formatBatchResult(stats, outPath, dry_run) {
1436
+ let res = "";
1437
+ if (dry_run) {
1438
+ res = `Dry-run simulation complete.
1439
+ `;
1440
+ } else {
1441
+ res = `Batch complete. Saved to: ${outPath}
1442
+ `;
1443
+ }
1444
+ res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
1445
+ `;
1446
+ res += `Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.
1447
+ `;
1448
+ if (stats.edits && stats.edits.length > 0) {
1449
+ res += "\nDetailed Edit Reports:\n";
1450
+ for (let i = 0; i < stats.edits.length; i++) {
1451
+ const report = stats.edits[i];
1452
+ const status_indicator = report.status === "applied" ? "\u2705 [applied]" : "\u274C [failed]";
1453
+ res += `Edit ${i + 1} ${status_indicator}:
1454
+ `;
1455
+ res += ` Target: '${report.target_text}'
1456
+ `;
1457
+ res += ` New text: '${report.new_text}'
1458
+ `;
1459
+ if (report.warning) {
1460
+ res += ` Warning: ${report.warning}
1461
+ `;
1462
+ }
1463
+ if (report.error) {
1464
+ res += ` Error: ${report.error}
1465
+ `;
1466
+ }
1467
+ if (report.critic_markup) {
1468
+ res += ` Preview (CriticMarkup): ${report.critic_markup}
1469
+ `;
1470
+ }
1471
+ if (report.clean_text) {
1472
+ res += ` Clean text preview: ${report.clean_text}
1473
+ `;
1474
+ }
1475
+ }
1476
+ }
1477
+ if (stats.skipped_details && stats.skipped_details.length > 0) {
1478
+ res += `
1479
+
1480
+ Skipped Details:
1481
+ ${stats.skipped_details.join("\n")}`;
1482
+ }
1483
+ return res;
1484
+ }
1146
1485
  async function main() {
1147
1486
  const transport = new StdioServerTransport();
1148
1487
  await server.connect(transport);
@@ -1151,4 +1490,7 @@ async function main() {
1151
1490
  );
1152
1491
  }
1153
1492
  main().catch(console.error);
1493
+ export {
1494
+ formatBatchResult
1495
+ };
1154
1496
  //# sourceMappingURL=index.js.map