@adeu/mcp-server 1.18.4 → 1.19.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.
@@ -10,7 +10,9 @@ const KNOWN_ERROR_HINTS: Record<string, string> = {
10
10
  "The email ID was not found. If this was a short ID (msg_*), it may have been " +
11
11
  "evicted from the local cache or come from a different machine — re-run " +
12
12
  "search_and_fetch_emails with filters to get a fresh ID. If it was an " +
13
- "adeu_<numeric> or raw provider ID, verify it's correct.",
13
+ "adeu_<numeric> or raw provider ID, verify it's correct. If the email lives in " +
14
+ "a shared or secondary mailbox, pass `mailbox_address` explicitly — provider " +
15
+ "IDs only resolve within the mailbox they came from.",
14
16
  "Adeu email reference not found.":
15
17
  "The adeu_<id> reference doesn't resolve to any processed email for this user. " +
16
18
  "Verify the ID, or re-run search_and_fetch_emails with filters to find the message.",
@@ -18,17 +20,7 @@ const KNOWN_ERROR_HINTS: Record<string, string> = {
18
20
  "The adeu_<id> reference is malformed. Expected format: adeu_<integer>.",
19
21
  };
20
22
 
21
- function formatBackendError(statusCode: number, responseBody: string): string {
22
- let detail = responseBody;
23
- try {
24
- const parsed = JSON.parse(responseBody);
25
- if (parsed && typeof parsed === "object" && "detail" in parsed) {
26
- detail = String(parsed.detail);
27
- }
28
- } catch {
29
- // responseBody isn't JSON — use it as-is
30
- }
31
-
23
+ function lookupErrorHint(detail: string): string | undefined {
32
24
  let hint = KNOWN_ERROR_HINTS[detail];
33
25
  if (
34
26
  !hint &&
@@ -41,8 +33,21 @@ function formatBackendError(statusCode: number, responseBody: string): string {
41
33
  "Call list_available_mailboxes to see valid mailbox addresses, then retry " +
42
34
  "with one of those as `mailbox_address`.";
43
35
  }
36
+ return hint;
37
+ }
38
+
39
+ function formatBackendError(statusCode: number, responseBody: string): string {
40
+ let detail = responseBody;
41
+ try {
42
+ const parsed = JSON.parse(responseBody);
43
+ if (parsed && typeof parsed === "object" && "detail" in parsed) {
44
+ detail = String(parsed.detail);
45
+ }
46
+ } catch {
47
+ // responseBody isn't JSON — use it as-is
48
+ }
44
49
 
45
- const message = hint ?? detail;
50
+ const message = lookupErrorHint(detail) ?? detail;
46
51
  return `Cloud search failed (HTTP ${statusCode}): ${message}`;
47
52
  }
48
53
  function isTimeoutError(err: unknown): boolean {
@@ -61,7 +66,22 @@ function formatBytes(bytes: number | null | undefined): string {
61
66
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
62
67
  }
63
68
 
64
- function loadIdCache(): Record<string, string> {
69
+ // Cache values are either legacy plain strings (the real provider ID) or
70
+ // objects {id, mailbox}. The mailbox matters: provider message IDs are
71
+ // mailbox-scoped, so a later fetch/reply must target the same mailbox or the
72
+ // backend resolves against the PRIMARY account and 404s with "Email not found."
73
+ type CacheEntry = string | { id?: string; mailbox?: string | null };
74
+
75
+ function cacheEntryParts(entry: CacheEntry | undefined): {
76
+ id: string | null;
77
+ mailbox: string | null;
78
+ } {
79
+ if (!entry) return { id: null, mailbox: null };
80
+ if (typeof entry === "string") return { id: entry, mailbox: null };
81
+ return { id: entry.id ?? null, mailbox: entry.mailbox ?? null };
82
+ }
83
+
84
+ function loadIdCache(): Record<string, CacheEntry> {
65
85
  if (existsSync(CACHE_FILE)) {
66
86
  try {
67
87
  return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
@@ -72,12 +92,12 @@ function loadIdCache(): Record<string, string> {
72
92
  return {};
73
93
  }
74
94
 
75
- function saveIdCache(cache: Record<string, string>): void {
95
+ function saveIdCache(cache: Record<string, CacheEntry>): void {
76
96
  try {
77
97
  mkdirSync(join(homedir(), ".adeu"), { recursive: true });
78
98
  const keys = Object.keys(cache);
79
99
  if (keys.length > MAX_CACHE_SIZE) {
80
- const trimmed: Record<string, string> = {};
100
+ const trimmed: Record<string, CacheEntry> = {};
81
101
  keys.slice(-MAX_CACHE_SIZE).forEach((k) => (trimmed[k] = cache[k]));
82
102
  cache = trimmed;
83
103
  }
@@ -87,11 +107,15 @@ function saveIdCache(cache: Record<string, string>): void {
87
107
  }
88
108
  }
89
109
 
90
- function minifyEmailId(realId: string, cache: Record<string, string>): string {
110
+ function minifyEmailId(
111
+ realId: string,
112
+ cache: Record<string, CacheEntry>,
113
+ mailboxAddress?: string | null,
114
+ ): string {
91
115
  if (!realId) return realId;
92
116
  const hash = createHash("md5").update(realId).digest("hex").slice(0, 6);
93
117
  const shortId = `msg_${hash}`;
94
- cache[shortId] = realId;
118
+ cache[shortId] = { id: realId, mailbox: mailboxAddress ?? null };
95
119
  return shortId;
96
120
  }
97
121
 
@@ -111,8 +135,8 @@ function resolveEmailId(shortId: string): string {
111
135
  // adeu_<id> references are resolved server-side, pass through.
112
136
  if (shortId.startsWith("adeu_")) return shortId;
113
137
  const cache = loadIdCache();
114
- const resolved = cache[shortId];
115
- if (resolved) return resolved;
138
+ const { id } = cacheEntryParts(cache[shortId]);
139
+ if (id) return id;
116
140
  // If it looks like one of our short IDs but isn't in the cache, fail loudly
117
141
  // instead of silently passing a meaningless string to the provider.
118
142
  if (shortId.startsWith("msg_")) {
@@ -122,6 +146,11 @@ function resolveEmailId(shortId: string): string {
122
146
  return shortId;
123
147
  }
124
148
 
149
+ function resolveCachedMailbox(shortId: string): string | null {
150
+ if (!shortId || !shortId.startsWith("msg_")) return null;
151
+ return cacheEntryParts(loadIdCache()[shortId]).mailbox;
152
+ }
153
+
125
154
  const HTML_NAMED_ENTITIES: Record<string, string> = {
126
155
  nbsp: " ",
127
156
  amp: "&",
@@ -341,7 +370,11 @@ async function pollEmailTask(taskId: string, apiKey: string): Promise<any> {
341
370
 
342
371
  if (status === "FAILED") {
343
372
  const errorMsg = taskData.error || "Unknown internal error";
344
- throw new Error(`Validation task failed on the server: ${errorMsg}`);
373
+ // Async failures carry the same recovery hints as sync HTTP errors —
374
+ // otherwise "Email not found." reaches the agent with no guidance and
375
+ // it improvises with fresh (often redundant) searches.
376
+ const hint = lookupErrorHint(errorMsg);
377
+ throw new Error(`Validation task failed on the server: ${hint ?? errorMsg}`);
345
378
  }
346
379
 
347
380
  // Wait 5 seconds before next poll
@@ -359,6 +392,11 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
359
392
  ? args.max_attachment_size_mb
360
393
  : 10;
361
394
 
395
+ // The mailbox this call actually targets. May be upgraded below from the
396
+ // short-ID cache: provider IDs are mailbox-scoped, so a fetch must go to the
397
+ // mailbox the ID was harvested from, not the user's primary.
398
+ let effectiveMailbox: string | undefined = args.mailbox_address;
399
+
362
400
  let data: any;
363
401
 
364
402
  if (args.task_id) {
@@ -398,6 +436,11 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
398
436
  throw err;
399
437
  }
400
438
 
439
+ if (args.email_id && !effectiveMailbox) {
440
+ const cachedMailbox = resolveCachedMailbox(args.email_id);
441
+ if (cachedMailbox) effectiveMailbox = cachedMailbox;
442
+ }
443
+
401
444
  const payload = {
402
445
  email_id: realEmailId,
403
446
  sender: args.sender,
@@ -409,7 +452,7 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
409
452
  folder: args.folder,
410
453
  limit: args.limit ?? 10,
411
454
  offset: args.offset ?? 0,
412
- mailbox_address: args.mailbox_address,
455
+ mailbox_address: effectiveMailbox,
413
456
  };
414
457
 
415
458
  // Remove undefined fields
@@ -479,6 +522,9 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
479
522
  text: "No emails found matching your search criteria.",
480
523
  },
481
524
  ],
525
+ // Keep the UI channel populated (Python parity) — without it the
526
+ // widget's tool-result handler bails and the skeleton spins forever.
527
+ structuredContent: data,
482
528
  };
483
529
 
484
530
  const lines = [
@@ -486,7 +532,7 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
486
532
  "",
487
533
  ];
488
534
  for (const p of previews) {
489
- const shortId = minifyEmailId(p.id, cache);
535
+ const shortId = minifyEmailId(p.id, cache, effectiveMailbox);
490
536
  const attFlag = p.has_attachments ? "📎 (Has Attachments)" : "";
491
537
  const unreadFlag = p.is_read === false ? "🟢 [UNREAD]" : "";
492
538
  lines.push(
@@ -515,7 +561,11 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
515
561
 
516
562
  if (data.type === "full_email") {
517
563
  const full = data.full_email || {};
518
- const shortTargetId = minifyEmailId(full.id || "unknown_id", cache);
564
+ const shortTargetId = minifyEmailId(
565
+ full.id || "unknown_id",
566
+ cache,
567
+ effectiveMailbox,
568
+ );
519
569
 
520
570
  saveIdCache(cache);
521
571
 
@@ -533,13 +583,30 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
533
583
  args.days_ago !== undefined ||
534
584
  args.folder !== undefined);
535
585
 
536
- const baseDir =
537
- args.working_directory && existsSync(args.working_directory)
538
- ? args.working_directory
539
- : tmpdir();
586
+ // Honor the requested working_directory by creating it (recursively) when
587
+ // missing. Silently falling back to the system temp dir made agents in
588
+ // sandboxed hosts believe the download failed (the reported paths pointed
589
+ // at an inaccessible /tmp), triggering redundant re-search loops.
590
+ let baseDir = tmpdir();
591
+ let usedWorkingDirectory = false;
592
+ let dirFallbackNote: string | null = null;
593
+ if (args.working_directory) {
594
+ try {
595
+ mkdirSync(args.working_directory, { recursive: true });
596
+ baseDir = args.working_directory;
597
+ usedWorkingDirectory = true;
598
+ } catch (e) {
599
+ dirFallbackNote =
600
+ `⚠️ **Attachment location notice**: the requested \`working_directory\` ` +
601
+ `(\`${args.working_directory}\`) did not exist and could not be created ` +
602
+ `(${(e as Error).message}). Any attachments were saved to the system temp ` +
603
+ `directory instead — use the exact paths listed below; do NOT re-run the ` +
604
+ `search expecting a different location.`;
605
+ }
606
+ }
540
607
  const saveDir = join(
541
608
  baseDir,
542
- args.working_directory ? "adeu_attachments" : "adeu_downloads",
609
+ usedWorkingDirectory ? "adeu_attachments" : "adeu_downloads",
543
610
  shortTargetId,
544
611
  );
545
612
  mkdirSync(saveDir, { recursive: true });
@@ -601,6 +668,9 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
601
668
  "_(Search returned exactly one result; auto-fetched full email below.)_\n",
602
669
  );
603
670
  }
671
+ if (dirFallbackNote) {
672
+ lines.push(dirFallbackNote + "\n");
673
+ }
604
674
  lines.push(
605
675
  `# Email Thread: ${full.subject}`,
606
676
  "",
@@ -667,7 +737,8 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
667
737
 
668
738
  if (hasAttachments) {
669
739
  lines.push(
670
- "\n*You can now use tools like `read_docx`, `diff_docx_files`, or `finalize_document` on the local file paths listed under each message.*",
740
+ "\n*You can now use tools like `read_docx`, `diff_docx_files`, or `finalize_document` on the local file paths listed under each message. " +
741
+ "These paths are on the user's machine — pass them directly to those tools; your own sandbox/shell may not see them, and that does NOT mean the download failed.*",
671
742
  );
672
743
  }
673
744
 
@@ -680,6 +751,7 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
680
751
  return {
681
752
  isError: true,
682
753
  content: [{ type: "text", text: "Unknown response format from backend." }],
754
+ structuredContent: data,
683
755
  };
684
756
  }
685
757
 
@@ -711,8 +783,16 @@ export async function create_email_draft(args: any): Promise<ToolResult> {
711
783
  }
712
784
  }
713
785
  if (args.subject) formData.append("subject", args.subject);
714
- if (args.mailbox_address) {
715
- formData.append("mailbox_address", args.mailbox_address);
786
+
787
+ // Replies inherit the mailbox the original email's short ID was harvested
788
+ // from unless the caller overrides — same mailbox-scoping rule as fetches.
789
+ let draftMailbox: string | undefined = args.mailbox_address;
790
+ if (args.reply_to_email_id && !draftMailbox) {
791
+ const cachedMailbox = resolveCachedMailbox(args.reply_to_email_id);
792
+ if (cachedMailbox) draftMailbox = cachedMailbox;
793
+ }
794
+ if (draftMailbox) {
795
+ formData.append("mailbox_address", draftMailbox);
716
796
  }
717
797
 
718
798
  if (args.to_recipients) {