@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.d.ts +2 -1
- package/dist/index.js +424 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/formatter.test.ts +64 -0
- package/src/index.ts +103 -19
- package/src/tools/auth.ts +9 -1
- 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();
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
/
|
|
416
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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)
|
|
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 ${
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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(
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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(
|
|
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.
|
|
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 ({
|
|
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
|
-
|
|
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")}`;
|
|
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
|
-
{
|
|
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
|
|
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
|