@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.
- package/README.md +16 -1
- package/dist/index.js +75 -28
- package/dist/index.js.map +1 -1
- package/dist/templates/email_ui.html +106 -3
- package/package.json +2 -2
- package/src/index.ts +11 -3
- package/src/templates/email_ui.html +106 -3
- package/src/tools/email.test.ts +281 -1
- package/src/tools/email.ts +112 -32
package/src/tools/email.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
|
115
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
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) {
|