@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.d.ts +2 -1
- package/dist/index.js +409 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/formatter.test.ts +64 -0
- package/src/index.ts +85 -18
- package/src/tools/auth.ts +1 -0
- package/src/tools/email.test.ts +258 -0
- package/src/tools/email.ts +438 -60
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
/
|
|
416
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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)
|
|
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 ${
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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(
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
|
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
|