@adeu/mcp-server 1.20.0 → 1.22.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.js +99 -1064
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +72 -236
- package/src/response-builders.ts +49 -1
- package/src/shared.ts +0 -5
- package/dist/templates/email_ui.html +0 -770
- package/src/desktop-auth.ts +0 -127
- package/src/mcp.cloud.test.ts +0 -152
- package/src/templates/email_ui.html +0 -770
- package/src/tools/auth.ts +0 -57
- package/src/tools/email.test.ts +0 -697
- package/src/tools/email.ts +0 -925
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { readFileSync
|
|
7
|
-
import { basename as basename2, resolve as resolve2, extname, dirname, join
|
|
6
|
+
import { readFileSync, existsSync } from "fs";
|
|
7
|
+
import { basename as basename2, resolve as resolve2, extname, dirname, join } from "path";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import {
|
|
10
10
|
registerAppTool as origRegisterAppTool,
|
|
@@ -22,13 +22,16 @@ import {
|
|
|
22
22
|
create_word_patch_diff,
|
|
23
23
|
finalize_document
|
|
24
24
|
} from "@adeu/core";
|
|
25
|
+
import { describe_illegal_control_chars } from "@adeu/core";
|
|
25
26
|
|
|
26
27
|
// src/response-builders.ts
|
|
27
28
|
import { resolve, basename } from "path";
|
|
28
29
|
import {
|
|
29
30
|
paginate,
|
|
30
31
|
split_structural_appendix,
|
|
31
|
-
extract_outline
|
|
32
|
+
extract_outline,
|
|
33
|
+
RegexTimeoutError,
|
|
34
|
+
userFindAllMatches
|
|
32
35
|
} from "@adeu/core";
|
|
33
36
|
function _build_appendix_pointer(has_appendix) {
|
|
34
37
|
if (!has_appendix) return "";
|
|
@@ -79,6 +82,21 @@ Document has ${nodes.length} headings, all at deeper levels. Call read_docx with
|
|
|
79
82
|
}
|
|
80
83
|
return lines.join("\n");
|
|
81
84
|
}
|
|
85
|
+
function build_full_document_response(text, file_path) {
|
|
86
|
+
const [body] = split_structural_appendix(text);
|
|
87
|
+
const ui_markdown = body;
|
|
88
|
+
const llm_content = `> **File Path:** \`${resolve(file_path)}\`
|
|
89
|
+
|
|
90
|
+
${ui_markdown}`;
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: llm_content }],
|
|
93
|
+
structuredContent: {
|
|
94
|
+
markdown: ui_markdown,
|
|
95
|
+
file_path: resolve(file_path),
|
|
96
|
+
title: basename(file_path)
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
82
100
|
function build_paginated_response(text, page, file_path) {
|
|
83
101
|
const [body, appendix] = split_structural_appendix(text);
|
|
84
102
|
const has_appendix = Boolean(appendix.trim());
|
|
@@ -111,6 +129,7 @@ ${ui_markdown}`;
|
|
|
111
129
|
};
|
|
112
130
|
}
|
|
113
131
|
function build_outline_response(doc, projected_text, file_path, outline_max_level = 2, outline_verbose = false, paragraph_offsets = null) {
|
|
132
|
+
outline_max_level = Math.max(1, Math.min(outline_max_level, 6));
|
|
114
133
|
const [body] = split_structural_appendix(projected_text);
|
|
115
134
|
const pagination_result = paginate(body, "");
|
|
116
135
|
const nodes = extract_outline(
|
|
@@ -206,9 +225,11 @@ function build_search_response(text, search_query, search_regex, search_case_sen
|
|
|
206
225
|
const flags = search_case_sensitive ? "g" : "gi";
|
|
207
226
|
let regexDowngradedNote = "";
|
|
208
227
|
let regex;
|
|
228
|
+
let isUserRegex = false;
|
|
209
229
|
if (search_regex) {
|
|
210
230
|
try {
|
|
211
231
|
regex = new RegExp(search_query, flags);
|
|
232
|
+
isUserRegex = true;
|
|
212
233
|
} catch (e) {
|
|
213
234
|
regexDowngradedNote = `> **Note:** \`${search_query}\` is not a valid regular expression (${e.message}), so it was searched as literal text instead. If you meant a regex, fix the pattern; if you meant literal text, set \`search_regex\` to false.`;
|
|
214
235
|
regex = new RegExp(escapeRegExp(search_query), flags);
|
|
@@ -216,7 +237,21 @@ function build_search_response(text, search_query, search_regex, search_case_sen
|
|
|
216
237
|
} else {
|
|
217
238
|
regex = new RegExp(escapeRegExp(search_query), flags);
|
|
218
239
|
}
|
|
219
|
-
|
|
240
|
+
let allMatches;
|
|
241
|
+
if (isUserRegex) {
|
|
242
|
+
try {
|
|
243
|
+
allMatches = userFindAllMatches(search_query, body, flags).map((m) => ({
|
|
244
|
+
0: body.slice(m.start, m.end),
|
|
245
|
+
index: m.start
|
|
246
|
+
}));
|
|
247
|
+
} catch (e) {
|
|
248
|
+
if (!(e instanceof RegexTimeoutError)) throw e;
|
|
249
|
+
regexDowngradedNote = `> **Note:** \`${search_query}\` was searched as literal text instead of as a regular expression: ${e.message}`;
|
|
250
|
+
allMatches = Array.from(body.matchAll(new RegExp(escapeRegExp(search_query), flags)));
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
allMatches = Array.from(body.matchAll(regex));
|
|
254
|
+
}
|
|
220
255
|
const pag_res = paginate(body, "");
|
|
221
256
|
const page_offsets = pag_res.body_page_offsets;
|
|
222
257
|
const total_doc_pages = pag_res.total_pages;
|
|
@@ -377,901 +412,8 @@ ${ui_markdown}`;
|
|
|
377
412
|
};
|
|
378
413
|
}
|
|
379
414
|
|
|
380
|
-
// src/desktop-auth.ts
|
|
381
|
-
import { createServer } from "http";
|
|
382
|
-
import { exec } from "child_process";
|
|
383
|
-
import { homedir, platform } from "os";
|
|
384
|
-
import { join } from "path";
|
|
385
|
-
import {
|
|
386
|
-
writeFileSync,
|
|
387
|
-
readFileSync,
|
|
388
|
-
mkdirSync,
|
|
389
|
-
existsSync,
|
|
390
|
-
rmSync,
|
|
391
|
-
chmodSync
|
|
392
|
-
} from "fs";
|
|
393
|
-
|
|
394
415
|
// src/shared.ts
|
|
395
|
-
var FRONTEND_URL = process.env.ADEU_FRONTEND_URL || "https://app.adeu.ai";
|
|
396
|
-
var BACKEND_URL = process.env.ADEU_BACKEND_URL || "https://app.adeu.ai";
|
|
397
416
|
var MARKDOWN_UI_URI = "ui://adeu/markdown-ui";
|
|
398
|
-
var EMAIL_UI_URI = "ui://adeu/email-ui";
|
|
399
|
-
|
|
400
|
-
// src/desktop-auth.ts
|
|
401
|
-
var ADEU_DIR = join(homedir(), ".adeu");
|
|
402
|
-
var CRED_PATH = join(ADEU_DIR, "credentials.json");
|
|
403
|
-
function openBrowser(url) {
|
|
404
|
-
if (platform() === "darwin") exec(`open "${url}"`);
|
|
405
|
-
else if (platform() === "win32") exec(`start "" "${url}"`);
|
|
406
|
-
else exec(`xdg-open "${url}"`);
|
|
407
|
-
}
|
|
408
|
-
var DesktopAuthManager = class {
|
|
409
|
-
static getApiKey() {
|
|
410
|
-
if (!existsSync(CRED_PATH)) return null;
|
|
411
|
-
try {
|
|
412
|
-
const data = JSON.parse(readFileSync(CRED_PATH, "utf-8"));
|
|
413
|
-
return data.api_key || null;
|
|
414
|
-
} catch {
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
static setApiKey(apiKey) {
|
|
419
|
-
if (!existsSync(ADEU_DIR)) {
|
|
420
|
-
mkdirSync(ADEU_DIR, { recursive: true });
|
|
421
|
-
}
|
|
422
|
-
writeFileSync(CRED_PATH, JSON.stringify({ api_key: apiKey }));
|
|
423
|
-
chmodSync(CRED_PATH, 384);
|
|
424
|
-
}
|
|
425
|
-
static clearApiKey() {
|
|
426
|
-
if (existsSync(CRED_PATH)) {
|
|
427
|
-
rmSync(CRED_PATH);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
static async authenticateInteractive() {
|
|
431
|
-
return new Promise((resolve3, reject) => {
|
|
432
|
-
let server2;
|
|
433
|
-
const timeout = setTimeout(
|
|
434
|
-
() => {
|
|
435
|
-
if (server2) server2.close();
|
|
436
|
-
reject(new Error("Authentication timed out after 5 minutes."));
|
|
437
|
-
},
|
|
438
|
-
5 * 60 * 1e3
|
|
439
|
-
);
|
|
440
|
-
server2 = createServer((req, res) => {
|
|
441
|
-
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
442
|
-
if (url.pathname === "/callback") {
|
|
443
|
-
const apiKey = url.searchParams.get("api_key");
|
|
444
|
-
res.writeHead(apiKey ? 200 : 400, { "Content-Type": "text/html" });
|
|
445
|
-
const title = apiKey ? "Authentication Successful!" : "Authentication Failed";
|
|
446
|
-
const text = apiKey ? "Your Adeu MCP server has been successfully authenticated. You can safely close this window and return to Claude." : "No API key received. Please try again.";
|
|
447
|
-
const color = apiKey ? "#107c10" : "#d83b01";
|
|
448
|
-
res.end(`
|
|
449
|
-
<!DOCTYPE html><html><head><title>${title}</title>
|
|
450
|
-
<style>body{font-family:sans-serif;text-align:center;padding:50px;background:#f3f2f1;}.container{background:white;padding:40px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1);max-width:500px;margin:0 auto;}h1{color:${color};}p{color:#605e5c;line-height:1.5;}</style>
|
|
451
|
-
</head><body><div class="container"><h1>${title}</h1><p>${text}</p>
|
|
452
|
-
<script>setTimeout(()=>window.close(), 3000);</script>
|
|
453
|
-
</div></body></html>
|
|
454
|
-
`);
|
|
455
|
-
clearTimeout(timeout);
|
|
456
|
-
setTimeout(() => server2.close(), 100);
|
|
457
|
-
if (apiKey) {
|
|
458
|
-
this.setApiKey(apiKey);
|
|
459
|
-
resolve3(apiKey);
|
|
460
|
-
} else {
|
|
461
|
-
reject(new Error("No API key received in callback."));
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
res.writeHead(404);
|
|
465
|
-
res.end();
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
server2.listen(0, "127.0.0.1", () => {
|
|
469
|
-
const address = server2.address();
|
|
470
|
-
if (address && typeof address !== "string") {
|
|
471
|
-
const authUrl = `${FRONTEND_URL}/login?desktop_port=${address.port}`;
|
|
472
|
-
openBrowser(authUrl);
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
static async ensureAuthenticated() {
|
|
478
|
-
const key = this.getApiKey();
|
|
479
|
-
if (key) return key;
|
|
480
|
-
return this.authenticateInteractive();
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
async function getCloudAuthToken() {
|
|
484
|
-
const key = DesktopAuthManager.getApiKey();
|
|
485
|
-
if (!key) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
"Authentication Required: You are not logged in. Please call the `login_to_adeu_cloud` tool first to authenticate, then try this task again."
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
return key;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// src/tools/auth.ts
|
|
494
|
-
async function login_to_adeu_cloud() {
|
|
495
|
-
try {
|
|
496
|
-
const apiKey = await DesktopAuthManager.ensureAuthenticated();
|
|
497
|
-
const res = await fetch(`${BACKEND_URL}/api/v1/auth/me`, {
|
|
498
|
-
headers: {
|
|
499
|
-
Authorization: `Bearer ${apiKey}`,
|
|
500
|
-
Accept: "application/json"
|
|
501
|
-
},
|
|
502
|
-
signal: AbortSignal.timeout(15e3)
|
|
503
|
-
});
|
|
504
|
-
if (res.status === 401) {
|
|
505
|
-
DesktopAuthManager.clearApiKey();
|
|
506
|
-
throw new Error(
|
|
507
|
-
"Your previous session expired. The stale key has been cleared. Please call `login_to_adeu_cloud` ONE MORE TIME to log in fresh."
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
|
511
|
-
const data = await res.json();
|
|
512
|
-
const email = data.email || "Unknown Email";
|
|
513
|
-
return {
|
|
514
|
-
content: [
|
|
515
|
-
{
|
|
516
|
-
type: "text",
|
|
517
|
-
text: `Login successful. You are now authenticated to Adeu Cloud as the user who owns the provider account \`${email}\` (the account used for SSO).
|
|
518
|
-
|
|
519
|
-
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.`
|
|
520
|
-
}
|
|
521
|
-
]
|
|
522
|
-
};
|
|
523
|
-
} catch (err) {
|
|
524
|
-
return { isError: true, content: [{ type: "text", text: err.message }] };
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
async function logout_of_adeu_cloud() {
|
|
528
|
-
DesktopAuthManager.clearApiKey();
|
|
529
|
-
return {
|
|
530
|
-
content: [
|
|
531
|
-
{
|
|
532
|
-
type: "text",
|
|
533
|
-
text: "Successfully logged out. The local API key has been removed."
|
|
534
|
-
}
|
|
535
|
-
]
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// src/tools/email.ts
|
|
540
|
-
import { homedir as homedir2, tmpdir } from "os";
|
|
541
|
-
import { join as join2 } from "path";
|
|
542
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
543
|
-
import { createHash } from "crypto";
|
|
544
|
-
var KNOWN_ERROR_HINTS = {
|
|
545
|
-
"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. If the email lives in a shared or secondary mailbox, pass `mailbox_address` explicitly \u2014 provider IDs only resolve within the mailbox they came from.",
|
|
546
|
-
"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.",
|
|
547
|
-
"Invalid adeu_ email ID format.": "The adeu_<id> reference is malformed. Expected format: adeu_<integer>."
|
|
548
|
-
};
|
|
549
|
-
function lookupErrorHint(detail) {
|
|
550
|
-
let hint = KNOWN_ERROR_HINTS[detail];
|
|
551
|
-
if (!hint && detail.startsWith("Mailbox '") && detail.endsWith("' not found.")) {
|
|
552
|
-
const mailbox = detail.slice("Mailbox '".length, -"' not found.".length);
|
|
553
|
-
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\`.`;
|
|
554
|
-
}
|
|
555
|
-
return hint;
|
|
556
|
-
}
|
|
557
|
-
function formatBackendError(statusCode, responseBody) {
|
|
558
|
-
let detail = responseBody;
|
|
559
|
-
try {
|
|
560
|
-
const parsed = JSON.parse(responseBody);
|
|
561
|
-
if (parsed && typeof parsed === "object" && "detail" in parsed) {
|
|
562
|
-
detail = String(parsed.detail);
|
|
563
|
-
}
|
|
564
|
-
} catch {
|
|
565
|
-
}
|
|
566
|
-
const message = lookupErrorHint(detail) ?? detail;
|
|
567
|
-
return `Cloud search failed (HTTP ${statusCode}): ${message}`;
|
|
568
|
-
}
|
|
569
|
-
function isTimeoutError(err) {
|
|
570
|
-
if (!err || typeof err !== "object") return false;
|
|
571
|
-
const name = err.name;
|
|
572
|
-
return name === "TimeoutError" || name === "AbortError";
|
|
573
|
-
}
|
|
574
|
-
var CACHE_FILE = join2(homedir2(), ".adeu", "mcp_id_cache.json");
|
|
575
|
-
var MAX_CACHE_SIZE = 1e3;
|
|
576
|
-
function formatBytes(bytes) {
|
|
577
|
-
if (bytes == null) return "unknown size";
|
|
578
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
579
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
580
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
581
|
-
}
|
|
582
|
-
function cacheEntryParts(entry) {
|
|
583
|
-
if (!entry) return { id: null, mailbox: null };
|
|
584
|
-
if (typeof entry === "string") return { id: entry, mailbox: null };
|
|
585
|
-
return { id: entry.id ?? null, mailbox: entry.mailbox ?? null };
|
|
586
|
-
}
|
|
587
|
-
function loadIdCache() {
|
|
588
|
-
if (existsSync2(CACHE_FILE)) {
|
|
589
|
-
try {
|
|
590
|
-
return JSON.parse(readFileSync2(CACHE_FILE, "utf-8"));
|
|
591
|
-
} catch {
|
|
592
|
-
return {};
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
return {};
|
|
596
|
-
}
|
|
597
|
-
function saveIdCache(cache) {
|
|
598
|
-
try {
|
|
599
|
-
mkdirSync2(join2(homedir2(), ".adeu"), { recursive: true });
|
|
600
|
-
const keys = Object.keys(cache);
|
|
601
|
-
if (keys.length > MAX_CACHE_SIZE) {
|
|
602
|
-
const trimmed = {};
|
|
603
|
-
keys.slice(-MAX_CACHE_SIZE).forEach((k) => trimmed[k] = cache[k]);
|
|
604
|
-
cache = trimmed;
|
|
605
|
-
}
|
|
606
|
-
writeFileSync2(CACHE_FILE, JSON.stringify(cache));
|
|
607
|
-
} catch {
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
function minifyEmailId(realId, cache, mailboxAddress) {
|
|
611
|
-
if (!realId) return realId;
|
|
612
|
-
const hash = createHash("md5").update(realId).digest("hex").slice(0, 6);
|
|
613
|
-
const shortId = `msg_${hash}`;
|
|
614
|
-
cache[shortId] = { id: realId, mailbox: mailboxAddress ?? null };
|
|
615
|
-
return shortId;
|
|
616
|
-
}
|
|
617
|
-
var StaleShortIdError = class extends Error {
|
|
618
|
-
constructor(shortId) {
|
|
619
|
-
super(
|
|
620
|
-
`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.`
|
|
621
|
-
);
|
|
622
|
-
this.name = "StaleShortIdError";
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
function resolveEmailId(shortId) {
|
|
626
|
-
if (!shortId) return shortId;
|
|
627
|
-
if (shortId.startsWith("adeu_")) return shortId;
|
|
628
|
-
const cache = loadIdCache();
|
|
629
|
-
const { id } = cacheEntryParts(cache[shortId]);
|
|
630
|
-
if (id) return id;
|
|
631
|
-
if (shortId.startsWith("msg_")) {
|
|
632
|
-
throw new StaleShortIdError(shortId);
|
|
633
|
-
}
|
|
634
|
-
return shortId;
|
|
635
|
-
}
|
|
636
|
-
function resolveCachedMailbox(shortId) {
|
|
637
|
-
if (!shortId || !shortId.startsWith("msg_")) return null;
|
|
638
|
-
return cacheEntryParts(loadIdCache()[shortId]).mailbox;
|
|
639
|
-
}
|
|
640
|
-
var HTML_NAMED_ENTITIES = {
|
|
641
|
-
nbsp: " ",
|
|
642
|
-
amp: "&",
|
|
643
|
-
lt: "<",
|
|
644
|
-
gt: ">",
|
|
645
|
-
quot: '"',
|
|
646
|
-
apos: "'",
|
|
647
|
-
copy: "\xA9",
|
|
648
|
-
reg: "\xAE",
|
|
649
|
-
trade: "\u2122",
|
|
650
|
-
hellip: "\u2026",
|
|
651
|
-
mdash: "\u2014",
|
|
652
|
-
ndash: "\u2013",
|
|
653
|
-
lsquo: "\u2018",
|
|
654
|
-
rsquo: "\u2019",
|
|
655
|
-
ldquo: "\u201C",
|
|
656
|
-
rdquo: "\u201D",
|
|
657
|
-
laquo: "\xAB",
|
|
658
|
-
raquo: "\xBB",
|
|
659
|
-
bull: "\u2022",
|
|
660
|
-
middot: "\xB7",
|
|
661
|
-
deg: "\xB0",
|
|
662
|
-
plusmn: "\xB1",
|
|
663
|
-
times: "\xD7",
|
|
664
|
-
divide: "\xF7",
|
|
665
|
-
euro: "\u20AC",
|
|
666
|
-
pound: "\xA3",
|
|
667
|
-
yen: "\xA5",
|
|
668
|
-
cent: "\xA2",
|
|
669
|
-
sect: "\xA7",
|
|
670
|
-
para: "\xB6",
|
|
671
|
-
iexcl: "\xA1",
|
|
672
|
-
iquest: "\xBF"
|
|
673
|
-
};
|
|
674
|
-
function decodeHtmlEntities(text) {
|
|
675
|
-
text = text.replace(/&#(\d+);/g, (_, dec) => {
|
|
676
|
-
const code = parseInt(dec, 10);
|
|
677
|
-
return Number.isFinite(code) ? String.fromCodePoint(code) : _;
|
|
678
|
-
});
|
|
679
|
-
text = text.replace(/&#[xX]([0-9a-fA-F]+);/g, (_, hex) => {
|
|
680
|
-
const code = parseInt(hex, 16);
|
|
681
|
-
return Number.isFinite(code) ? String.fromCodePoint(code) : _;
|
|
682
|
-
});
|
|
683
|
-
text = text.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name) => {
|
|
684
|
-
const replacement = HTML_NAMED_ENTITIES[name.toLowerCase()];
|
|
685
|
-
return replacement !== void 0 ? replacement : match;
|
|
686
|
-
});
|
|
687
|
-
return text;
|
|
688
|
-
}
|
|
689
|
-
function stripTags(html) {
|
|
690
|
-
if (!html) return "";
|
|
691
|
-
let text = html;
|
|
692
|
-
const suppressPattern = /<(style|script|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
693
|
-
let prev;
|
|
694
|
-
do {
|
|
695
|
-
prev = text;
|
|
696
|
-
text = text.replace(suppressPattern, "");
|
|
697
|
-
} while (text !== prev);
|
|
698
|
-
text = text.replace(/<(style|script|head|title)\b[^>]*>[\s\S]*$/gi, "");
|
|
699
|
-
text = text.replace(
|
|
700
|
-
/<\/?(p|div|br|hr|tr|li|h[1-6]|blockquote)\b[^>]*>/gi,
|
|
701
|
-
"\n"
|
|
702
|
-
);
|
|
703
|
-
text = text.replace(/<[^>]+>/g, "");
|
|
704
|
-
text = decodeHtmlEntities(text);
|
|
705
|
-
return text.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
|
|
706
|
-
}
|
|
707
|
-
function removeNestedQuotes(text) {
|
|
708
|
-
if (!text) return "";
|
|
709
|
-
const fromTokens = [
|
|
710
|
-
"From",
|
|
711
|
-
// English
|
|
712
|
-
"L\xE4hett\xE4j\xE4",
|
|
713
|
-
// Finnish
|
|
714
|
-
"Fr\xE5n",
|
|
715
|
-
// Swedish
|
|
716
|
-
"Von",
|
|
717
|
-
// German
|
|
718
|
-
"De",
|
|
719
|
-
// French / Spanish / Portuguese
|
|
720
|
-
"Da",
|
|
721
|
-
// Italian
|
|
722
|
-
"Van",
|
|
723
|
-
// Dutch
|
|
724
|
-
"Fra",
|
|
725
|
-
// Norwegian / Danish
|
|
726
|
-
"Mittente"
|
|
727
|
-
// Italian (alt)
|
|
728
|
-
];
|
|
729
|
-
const sentTokens = [
|
|
730
|
-
"Sent",
|
|
731
|
-
"L\xE4hetetty",
|
|
732
|
-
"Skickat",
|
|
733
|
-
"Gesendet",
|
|
734
|
-
"Envoy\xE9",
|
|
735
|
-
"Enviado",
|
|
736
|
-
"Inviato",
|
|
737
|
-
"Verzonden",
|
|
738
|
-
"Sendt"
|
|
739
|
-
];
|
|
740
|
-
const wrotePatterns = [
|
|
741
|
-
/On .{1,200}? wrote:/,
|
|
742
|
-
// English
|
|
743
|
-
/Le .{1,200}? a écrit\s*:/i,
|
|
744
|
-
// French
|
|
745
|
-
/Am .{1,200}? schrieb .{1,100}?:/i,
|
|
746
|
-
// German
|
|
747
|
-
/El .{1,200}? escribió\s*:/i,
|
|
748
|
-
// Spanish
|
|
749
|
-
/Il .{1,200}? ha scritto\s*:/i,
|
|
750
|
-
// Italian
|
|
751
|
-
/Op .{1,200}? schreef .{1,100}?:/i,
|
|
752
|
-
// Dutch
|
|
753
|
-
/Den .{1,200}? skrev .{1,100}?:/i,
|
|
754
|
-
// Swedish/Norwegian/Danish
|
|
755
|
-
/Em .{1,200}? escreveu\s*:/i,
|
|
756
|
-
// Portuguese
|
|
757
|
-
/Em\b.{1,200}?, .{1,200}? escreveu\s*:/i,
|
|
758
|
-
// Portuguese (date prefix)
|
|
759
|
-
new RegExp(
|
|
760
|
-
`^(${fromTokens.join("|")})\\s*:.*?\\n(?:.*\\n){0,5}?(${sentTokens.join("|")})\\s*:`,
|
|
761
|
-
"m"
|
|
762
|
-
)
|
|
763
|
-
];
|
|
764
|
-
const forwardedTokens = [
|
|
765
|
-
"Forwarded message",
|
|
766
|
-
"V\xE4litetty viesti",
|
|
767
|
-
"Vidarebefordrat meddelande",
|
|
768
|
-
"Weitergeleitete Nachricht",
|
|
769
|
-
"Message transf\xE9r\xE9",
|
|
770
|
-
"Mensaje reenviado",
|
|
771
|
-
"Messaggio inoltrato",
|
|
772
|
-
"Doorgestuurd bericht",
|
|
773
|
-
"Videresendt melding",
|
|
774
|
-
"Videresendt meddelelse",
|
|
775
|
-
"Mensagem encaminhada"
|
|
776
|
-
].join("|");
|
|
777
|
-
const dividerPatterns = [
|
|
778
|
-
/_{10,}/m,
|
|
779
|
-
/-----\s*(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht|Original meddelande)\s*-----/im,
|
|
780
|
-
/^(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht)$/im,
|
|
781
|
-
// Gmail/Outlook-style "---------- Forwarded message ---------" with localized variants
|
|
782
|
-
new RegExp(`-+\\s*(${forwardedTokens})\\s*-+`, "i"),
|
|
783
|
-
new RegExp(`^(${forwardedTokens})$`, "im")
|
|
784
|
-
];
|
|
785
|
-
const allPatterns = [...wrotePatterns, ...dividerPatterns];
|
|
786
|
-
let earliestCut = text.length;
|
|
787
|
-
for (const pattern of allPatterns) {
|
|
788
|
-
const match = pattern.exec(text);
|
|
789
|
-
if (match && match.index < earliestCut) {
|
|
790
|
-
earliestCut = match.index;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
return text.substring(0, earliestCut).trim();
|
|
794
|
-
}
|
|
795
|
-
function getUniqueFilepath(saveDir, filename) {
|
|
796
|
-
return join2(saveDir, filename);
|
|
797
|
-
}
|
|
798
|
-
async function pollEmailTask(taskId, apiKey) {
|
|
799
|
-
const pollUrl = `${BACKEND_URL}/api/v1/emails/tasks/${taskId}`;
|
|
800
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
801
|
-
let res;
|
|
802
|
-
try {
|
|
803
|
-
res = await fetch(pollUrl, {
|
|
804
|
-
headers: {
|
|
805
|
-
Authorization: `Bearer ${apiKey}`,
|
|
806
|
-
Accept: "application/json"
|
|
807
|
-
},
|
|
808
|
-
signal: AbortSignal.timeout(15e3)
|
|
809
|
-
});
|
|
810
|
-
} catch (err) {
|
|
811
|
-
if (isTimeoutError(err)) {
|
|
812
|
-
throw new Error("Checking task status timed out.");
|
|
813
|
-
}
|
|
814
|
-
throw err;
|
|
815
|
-
}
|
|
816
|
-
if (res.status === 401) {
|
|
817
|
-
DesktopAuthManager.clearApiKey();
|
|
818
|
-
throw new Error(
|
|
819
|
-
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
if (!res.ok) {
|
|
823
|
-
throw new Error(formatBackendError(res.status, await res.text()));
|
|
824
|
-
}
|
|
825
|
-
const taskData = await res.json();
|
|
826
|
-
const status = taskData.status;
|
|
827
|
-
if (status === "COMPLETED") {
|
|
828
|
-
return taskData;
|
|
829
|
-
}
|
|
830
|
-
if (status === "FAILED") {
|
|
831
|
-
const errorMsg = taskData.error || "Unknown internal error";
|
|
832
|
-
const hint = lookupErrorHint(errorMsg);
|
|
833
|
-
throw new Error(`Validation task failed on the server: ${hint ?? errorMsg}`);
|
|
834
|
-
}
|
|
835
|
-
await new Promise((resolve3) => setTimeout(resolve3, 5e3));
|
|
836
|
-
}
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
async function search_and_fetch_emails(args2) {
|
|
840
|
-
const apiKey = await getCloudAuthToken();
|
|
841
|
-
const maxAttachmentSizeMb = typeof args2.max_attachment_size_mb === "number" && args2.max_attachment_size_mb > 0 ? args2.max_attachment_size_mb : 10;
|
|
842
|
-
let effectiveMailbox = args2.mailbox_address;
|
|
843
|
-
let data;
|
|
844
|
-
if (args2.task_id) {
|
|
845
|
-
const completedData = await pollEmailTask(args2.task_id, apiKey);
|
|
846
|
-
if (!completedData) {
|
|
847
|
-
const msg = `Task ${args2.task_id} is still processing. Please call \`search_and_fetch_emails\` again with task_id=${args2.task_id}.`;
|
|
848
|
-
return {
|
|
849
|
-
content: [{ type: "text", text: msg }],
|
|
850
|
-
structuredContent: {
|
|
851
|
-
status: "pending",
|
|
852
|
-
task_id: args2.task_id,
|
|
853
|
-
message: msg
|
|
854
|
-
}
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
data = completedData;
|
|
858
|
-
} else {
|
|
859
|
-
let realEmailId;
|
|
860
|
-
try {
|
|
861
|
-
realEmailId = args2.email_id ? resolveEmailId(args2.email_id) : void 0;
|
|
862
|
-
} catch (err) {
|
|
863
|
-
if (err instanceof StaleShortIdError) {
|
|
864
|
-
return {
|
|
865
|
-
isError: true,
|
|
866
|
-
content: [{ type: "text", text: err.message }]
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
throw err;
|
|
870
|
-
}
|
|
871
|
-
if (args2.email_id && !effectiveMailbox) {
|
|
872
|
-
const cachedMailbox = resolveCachedMailbox(args2.email_id);
|
|
873
|
-
if (cachedMailbox) effectiveMailbox = cachedMailbox;
|
|
874
|
-
}
|
|
875
|
-
const payload = {
|
|
876
|
-
email_id: realEmailId,
|
|
877
|
-
sender: args2.sender,
|
|
878
|
-
subject: args2.subject,
|
|
879
|
-
has_attachments: args2.has_attachments,
|
|
880
|
-
attachment_name: args2.attachment_name,
|
|
881
|
-
is_unread: args2.is_unread,
|
|
882
|
-
days_ago: args2.days_ago,
|
|
883
|
-
folder: args2.folder,
|
|
884
|
-
limit: args2.limit ?? 10,
|
|
885
|
-
offset: args2.offset ?? 0,
|
|
886
|
-
mailbox_address: effectiveMailbox
|
|
887
|
-
};
|
|
888
|
-
Object.keys(payload).forEach(
|
|
889
|
-
(k) => payload[k] === void 0 && delete payload[k]
|
|
890
|
-
);
|
|
891
|
-
let res;
|
|
892
|
-
try {
|
|
893
|
-
res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
|
|
894
|
-
method: "POST",
|
|
895
|
-
headers: {
|
|
896
|
-
Authorization: `Bearer ${apiKey}`,
|
|
897
|
-
"Content-Type": "application/json"
|
|
898
|
-
},
|
|
899
|
-
body: JSON.stringify(payload),
|
|
900
|
-
signal: AbortSignal.timeout(45e3)
|
|
901
|
-
});
|
|
902
|
-
} catch (err) {
|
|
903
|
-
if (isTimeoutError(err)) {
|
|
904
|
-
throw new Error(
|
|
905
|
-
"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."
|
|
906
|
-
);
|
|
907
|
-
}
|
|
908
|
-
throw err;
|
|
909
|
-
}
|
|
910
|
-
if (res.status === 401) {
|
|
911
|
-
DesktopAuthManager.clearApiKey();
|
|
912
|
-
throw new Error(
|
|
913
|
-
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
if (!res.ok)
|
|
917
|
-
throw new Error(formatBackendError(res.status, await res.text()));
|
|
918
|
-
data = await res.json();
|
|
919
|
-
if (res.status === 202 || data && (data.status === "pending" || data.task_id) && data.type === void 0) {
|
|
920
|
-
const newTaskId = data.task_id;
|
|
921
|
-
const completedData = await pollEmailTask(String(newTaskId), apiKey);
|
|
922
|
-
if (!completedData) {
|
|
923
|
-
const msg = `Task ${newTaskId} is still processing. Please call \`search_and_fetch_emails\` again immediately with task_id=${newTaskId} to monitor the progress.`;
|
|
924
|
-
return {
|
|
925
|
-
content: [{ type: "text", text: msg }],
|
|
926
|
-
structuredContent: {
|
|
927
|
-
status: "pending",
|
|
928
|
-
task_id: String(newTaskId),
|
|
929
|
-
message: msg
|
|
930
|
-
}
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
data = completedData;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
const cache = loadIdCache();
|
|
937
|
-
if (data.type === "previews") {
|
|
938
|
-
const previews = data.previews || [];
|
|
939
|
-
if (!previews.length)
|
|
940
|
-
return {
|
|
941
|
-
content: [
|
|
942
|
-
{
|
|
943
|
-
type: "text",
|
|
944
|
-
text: "No emails found matching your search criteria."
|
|
945
|
-
}
|
|
946
|
-
],
|
|
947
|
-
// Keep the UI channel populated (Python parity) — without it the
|
|
948
|
-
// widget's tool-result handler bails and the skeleton spins forever.
|
|
949
|
-
structuredContent: data
|
|
950
|
-
};
|
|
951
|
-
const lines = [
|
|
952
|
-
`Found ${previews.length} email(s). Here are the previews:`,
|
|
953
|
-
""
|
|
954
|
-
];
|
|
955
|
-
for (const p of previews) {
|
|
956
|
-
const shortId = minifyEmailId(p.id, cache, effectiveMailbox);
|
|
957
|
-
const attFlag = p.has_attachments ? "\u{1F4CE} (Has Attachments)" : "";
|
|
958
|
-
const unreadFlag = p.is_read === false ? "\u{1F7E2} [UNREAD]" : "";
|
|
959
|
-
lines.push(
|
|
960
|
-
`- **ID**: \`${shortId}\`
|
|
961
|
-
**Subject**: ${p.subject} ${attFlag} ${unreadFlag}
|
|
962
|
-
**From**: ${p.sender_name} <${p.sender_email}>
|
|
963
|
-
**Date**: ${p.received_datetime}
|
|
964
|
-
**Preview**: ${p.preview_text}
|
|
965
|
-
`
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
saveIdCache(cache);
|
|
969
|
-
const limit = typeof args2.limit === "number" ? args2.limit : 10;
|
|
970
|
-
const offset = typeof args2.offset === "number" ? args2.offset : 0;
|
|
971
|
-
const pageHint = previews.length >= limit ? `
|
|
972
|
-
*(If you need to see more results, call this tool again with offset=${offset + limit})*` : "";
|
|
973
|
-
lines.push(
|
|
974
|
-
"\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
|
|
975
|
-
);
|
|
976
|
-
return {
|
|
977
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
978
|
-
structuredContent: data
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
if (data.type === "full_email") {
|
|
982
|
-
const full = data.full_email || {};
|
|
983
|
-
const shortTargetId = minifyEmailId(
|
|
984
|
-
full.id || "unknown_id",
|
|
985
|
-
cache,
|
|
986
|
-
effectiveMailbox
|
|
987
|
-
);
|
|
988
|
-
saveIdCache(cache);
|
|
989
|
-
const autoEscalated = !args2.email_id && (args2.sender !== void 0 || args2.subject !== void 0 || args2.has_attachments !== void 0 || args2.attachment_name !== void 0 || args2.is_unread !== void 0 || args2.days_ago !== void 0 || args2.folder !== void 0);
|
|
990
|
-
let baseDir = tmpdir();
|
|
991
|
-
let usedWorkingDirectory = false;
|
|
992
|
-
let dirFallbackNote = null;
|
|
993
|
-
if (args2.working_directory) {
|
|
994
|
-
try {
|
|
995
|
-
mkdirSync2(args2.working_directory, { recursive: true });
|
|
996
|
-
baseDir = args2.working_directory;
|
|
997
|
-
usedWorkingDirectory = true;
|
|
998
|
-
} catch (e) {
|
|
999
|
-
dirFallbackNote = `\u26A0\uFE0F **Attachment location notice**: the requested \`working_directory\` (\`${args2.working_directory}\`) did not exist and could not be created (${e.message}). Any attachments were saved to the system temp directory instead \u2014 use the exact paths listed below; do NOT re-run the search expecting a different location.`;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
const saveDir = join2(
|
|
1003
|
-
baseDir,
|
|
1004
|
-
usedWorkingDirectory ? "adeu_attachments" : "adeu_downloads",
|
|
1005
|
-
shortTargetId
|
|
1006
|
-
);
|
|
1007
|
-
mkdirSync2(saveDir, { recursive: true });
|
|
1008
|
-
async function processAttachments(msg) {
|
|
1009
|
-
const localFiles = [];
|
|
1010
|
-
const skipped = [];
|
|
1011
|
-
const maxBytes = maxAttachmentSizeMb * 1024 * 1024;
|
|
1012
|
-
for (const att of msg.attachments || []) {
|
|
1013
|
-
const filename = att.filename || "unnamed_file";
|
|
1014
|
-
const size = typeof att.size_bytes === "number" ? att.size_bytes : null;
|
|
1015
|
-
if (size != null && size > maxBytes) {
|
|
1016
|
-
skipped.push({
|
|
1017
|
-
filename,
|
|
1018
|
-
size_bytes: size,
|
|
1019
|
-
reason: `exceeds ${maxAttachmentSizeMb} MB cap`
|
|
1020
|
-
});
|
|
1021
|
-
delete att.base64_data;
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
if (att.base64_data) {
|
|
1025
|
-
try {
|
|
1026
|
-
const filepath = getUniqueFilepath(saveDir, filename);
|
|
1027
|
-
writeFileSync2(filepath, Buffer.from(att.base64_data, "base64"));
|
|
1028
|
-
localFiles.push(filepath);
|
|
1029
|
-
att.local_path = filepath;
|
|
1030
|
-
delete att.base64_data;
|
|
1031
|
-
} catch (e) {
|
|
1032
|
-
console.error(`Failed to save attachment ${filename}`, e);
|
|
1033
|
-
skipped.push({
|
|
1034
|
-
filename,
|
|
1035
|
-
size_bytes: size,
|
|
1036
|
-
reason: `download failed: ${e.message}`
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
return { localFiles, skipped };
|
|
1042
|
-
}
|
|
1043
|
-
const { localFiles: targetFiles, skipped: targetSkipped } = await processAttachments(full);
|
|
1044
|
-
const lines = [];
|
|
1045
|
-
if (autoEscalated) {
|
|
1046
|
-
lines.push(
|
|
1047
|
-
"_(Search returned exactly one result; auto-fetched full email below.)_\n"
|
|
1048
|
-
);
|
|
1049
|
-
}
|
|
1050
|
-
if (dirFallbackNote) {
|
|
1051
|
-
lines.push(dirFallbackNote + "\n");
|
|
1052
|
-
}
|
|
1053
|
-
lines.push(
|
|
1054
|
-
`# Email Thread: ${full.subject}`,
|
|
1055
|
-
"",
|
|
1056
|
-
"## Target Message (Newest):",
|
|
1057
|
-
`**From**: ${full.sender_name} <${full.sender_email}>`,
|
|
1058
|
-
`**Date**: ${full.received_datetime}`
|
|
1059
|
-
);
|
|
1060
|
-
if (targetFiles.length) {
|
|
1061
|
-
lines.push("**Attachments Saved Locally**:");
|
|
1062
|
-
targetFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
|
|
1063
|
-
}
|
|
1064
|
-
if (targetSkipped.length) {
|
|
1065
|
-
lines.push(
|
|
1066
|
-
`**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` to raise the ${maxAttachmentSizeMb} MB cap:`
|
|
1067
|
-
);
|
|
1068
|
-
targetSkipped.forEach(
|
|
1069
|
-
(s) => lines.push(
|
|
1070
|
-
`- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
|
|
1071
|
-
)
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
const cleanBody = removeNestedQuotes(stripTags(full.body_html || ""));
|
|
1075
|
-
lines.push(`**Body**:
|
|
1076
|
-
\`\`\`
|
|
1077
|
-
${cleanBody}
|
|
1078
|
-
\`\`\`
|
|
1079
|
-
`);
|
|
1080
|
-
if (full.is_thread && full.messages?.length) {
|
|
1081
|
-
lines.push("## Previous Messages in Thread (Historical Context):");
|
|
1082
|
-
for (let i = 0; i < full.messages.length; i++) {
|
|
1083
|
-
const histMsg = full.messages[i];
|
|
1084
|
-
const { localFiles: histFiles, skipped: histSkipped } = await processAttachments(histMsg);
|
|
1085
|
-
lines.push(
|
|
1086
|
-
`### Message -${i + 1} (Older)
|
|
1087
|
-
**From**: ${histMsg.sender_name} <${histMsg.sender_email}>
|
|
1088
|
-
**Date**: ${histMsg.received_datetime}`
|
|
1089
|
-
);
|
|
1090
|
-
if (histFiles.length) {
|
|
1091
|
-
lines.push("**Attachments Saved Locally**:");
|
|
1092
|
-
histFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
|
|
1093
|
-
}
|
|
1094
|
-
if (histSkipped.length) {
|
|
1095
|
-
lines.push(
|
|
1096
|
-
`**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` \u2014 raise the cap:`
|
|
1097
|
-
);
|
|
1098
|
-
histSkipped.forEach(
|
|
1099
|
-
(s) => lines.push(
|
|
1100
|
-
`- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
|
|
1101
|
-
)
|
|
1102
|
-
);
|
|
1103
|
-
}
|
|
1104
|
-
lines.push(
|
|
1105
|
-
`**Body**:
|
|
1106
|
-
\`\`\`
|
|
1107
|
-
${removeNestedQuotes(stripTags(histMsg.body_html || ""))}
|
|
1108
|
-
\`\`\`
|
|
1109
|
-
`
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
const hasAttachments = targetFiles.length > 0 || full.messages && full.messages.some(
|
|
1114
|
-
(m) => m.attachments && m.attachments.length > 0
|
|
1115
|
-
);
|
|
1116
|
-
if (hasAttachments) {
|
|
1117
|
-
lines.push(
|
|
1118
|
-
"\n*You can now use tools like `read_docx`, `diff_docx_files`, or `finalize_document` on the local file paths listed under each message. These paths are on the user's machine \u2014 pass them directly to those tools; your own sandbox/shell may not see them, and that does NOT mean the download failed.*"
|
|
1119
|
-
);
|
|
1120
|
-
}
|
|
1121
|
-
return {
|
|
1122
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
1123
|
-
structuredContent: data
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
return {
|
|
1127
|
-
isError: true,
|
|
1128
|
-
content: [{ type: "text", text: "Unknown response format from backend." }],
|
|
1129
|
-
structuredContent: data
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
async function create_email_draft(args2) {
|
|
1133
|
-
const apiKey = await getCloudAuthToken();
|
|
1134
|
-
if (!args2.reply_to_email_id && (!args2.subject || !args2.to_recipients)) {
|
|
1135
|
-
throw new Error(
|
|
1136
|
-
"You must provide either 'reply_to_email_id' OR both 'subject' and 'to_recipients'."
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
const formData = new FormData();
|
|
1140
|
-
formData.append("body_markdown", args2.body_markdown);
|
|
1141
|
-
if (args2.reply_to_email_id) {
|
|
1142
|
-
try {
|
|
1143
|
-
formData.append(
|
|
1144
|
-
"reply_to_email_id",
|
|
1145
|
-
resolveEmailId(args2.reply_to_email_id)
|
|
1146
|
-
);
|
|
1147
|
-
} catch (err) {
|
|
1148
|
-
if (err instanceof StaleShortIdError) {
|
|
1149
|
-
return {
|
|
1150
|
-
isError: true,
|
|
1151
|
-
content: [{ type: "text", text: err.message }]
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
throw err;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
if (args2.subject) formData.append("subject", args2.subject);
|
|
1158
|
-
let draftMailbox = args2.mailbox_address;
|
|
1159
|
-
if (args2.reply_to_email_id && !draftMailbox) {
|
|
1160
|
-
const cachedMailbox = resolveCachedMailbox(args2.reply_to_email_id);
|
|
1161
|
-
if (cachedMailbox) draftMailbox = cachedMailbox;
|
|
1162
|
-
}
|
|
1163
|
-
if (draftMailbox) {
|
|
1164
|
-
formData.append("mailbox_address", draftMailbox);
|
|
1165
|
-
}
|
|
1166
|
-
if (args2.to_recipients) {
|
|
1167
|
-
const recips = typeof args2.to_recipients === "string" ? JSON.parse(args2.to_recipients) : args2.to_recipients;
|
|
1168
|
-
formData.append("to_recipients", JSON.stringify(recips));
|
|
1169
|
-
}
|
|
1170
|
-
if (args2.attachment_paths) {
|
|
1171
|
-
const paths = typeof args2.attachment_paths === "string" ? JSON.parse(args2.attachment_paths) : args2.attachment_paths;
|
|
1172
|
-
for (const p of paths) {
|
|
1173
|
-
const buf = readFileSync2(p);
|
|
1174
|
-
const filename = p.split(/[/\\]/).pop();
|
|
1175
|
-
formData.append("files", new Blob([buf]), filename);
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
let res;
|
|
1179
|
-
try {
|
|
1180
|
-
res = await fetch(`${BACKEND_URL}/api/v1/emails/drafts/new`, {
|
|
1181
|
-
method: "POST",
|
|
1182
|
-
headers: {
|
|
1183
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1184
|
-
Accept: "application/json"
|
|
1185
|
-
},
|
|
1186
|
-
body: formData,
|
|
1187
|
-
signal: AbortSignal.timeout(9e4)
|
|
1188
|
-
});
|
|
1189
|
-
} catch (err) {
|
|
1190
|
-
if (isTimeoutError(err)) {
|
|
1191
|
-
throw new Error(
|
|
1192
|
-
"Draft creation timed out after 90s. If the draft includes large attachments, try splitting them across multiple drafts or omitting the largest files."
|
|
1193
|
-
);
|
|
1194
|
-
}
|
|
1195
|
-
throw err;
|
|
1196
|
-
}
|
|
1197
|
-
if (res.status === 401) {
|
|
1198
|
-
DesktopAuthManager.clearApiKey();
|
|
1199
|
-
throw new Error(
|
|
1200
|
-
"Authentication expired. Please call `login_to_adeu_cloud`."
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
if (!res.ok)
|
|
1204
|
-
throw new Error(formatBackendError(res.status, await res.text()));
|
|
1205
|
-
const data = await res.json();
|
|
1206
|
-
return {
|
|
1207
|
-
content: [
|
|
1208
|
-
{
|
|
1209
|
-
type: "text",
|
|
1210
|
-
text: `Successfully created email draft! Draft ID: ${data.id}`
|
|
1211
|
-
}
|
|
1212
|
-
]
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
async function list_available_mailboxes() {
|
|
1216
|
-
const apiKey = await getCloudAuthToken();
|
|
1217
|
-
let res;
|
|
1218
|
-
try {
|
|
1219
|
-
res = await fetch(`${BACKEND_URL}/api/v1/users/me/shared-mailboxes`, {
|
|
1220
|
-
method: "GET",
|
|
1221
|
-
headers: {
|
|
1222
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1223
|
-
Accept: "application/json"
|
|
1224
|
-
},
|
|
1225
|
-
signal: AbortSignal.timeout(15e3)
|
|
1226
|
-
});
|
|
1227
|
-
} catch (err) {
|
|
1228
|
-
if (isTimeoutError(err)) {
|
|
1229
|
-
throw new Error(
|
|
1230
|
-
"Listing mailboxes timed out after 15s. The Adeu backend may be temporarily unavailable; retry shortly."
|
|
1231
|
-
);
|
|
1232
|
-
}
|
|
1233
|
-
throw err;
|
|
1234
|
-
}
|
|
1235
|
-
if (res.status === 401) {
|
|
1236
|
-
DesktopAuthManager.clearApiKey();
|
|
1237
|
-
throw new Error(
|
|
1238
|
-
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
|
|
1239
|
-
);
|
|
1240
|
-
}
|
|
1241
|
-
if (!res.ok) {
|
|
1242
|
-
throw new Error(formatBackendError(res.status, await res.text()));
|
|
1243
|
-
}
|
|
1244
|
-
const mailboxes = await res.json();
|
|
1245
|
-
if (!mailboxes.length) {
|
|
1246
|
-
return {
|
|
1247
|
-
content: [
|
|
1248
|
-
{
|
|
1249
|
-
type: "text",
|
|
1250
|
-
text: "No configured mailboxes found for your profile."
|
|
1251
|
-
}
|
|
1252
|
-
]
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
mailboxes.sort(
|
|
1256
|
-
(a, b) => (a.email_address ?? "").toLowerCase().localeCompare((b.email_address ?? "").toLowerCase())
|
|
1257
|
-
);
|
|
1258
|
-
const lines = [
|
|
1259
|
-
"### Connected Mailboxes",
|
|
1260
|
-
"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:",
|
|
1261
|
-
""
|
|
1262
|
-
];
|
|
1263
|
-
for (const box of mailboxes) {
|
|
1264
|
-
lines.push(
|
|
1265
|
-
`- **${box.display_name || "Personal Mailbox"}**
|
|
1266
|
-
- **Email Address**: \`${box.email_address}\`
|
|
1267
|
-
- **Auto-Processing**: ${box.auto_process_enabled ? "Enabled" : "Disabled"}
|
|
1268
|
-
- **Write-Back Mode**: \`${box.write_back_preference}\``
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
return {
|
|
1272
|
-
content: [{ type: "text", text: lines.join("\n") }]
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
417
|
|
|
1276
418
|
// src/index.ts
|
|
1277
419
|
var MATCH_MODE_SYNONYMS = {
|
|
@@ -1306,7 +448,7 @@ function coerceChangeItemInPlace(item) {
|
|
|
1306
448
|
}
|
|
1307
449
|
function readFileBytesOrThrow(filePath) {
|
|
1308
450
|
try {
|
|
1309
|
-
return
|
|
451
|
+
return readFileSync(filePath);
|
|
1310
452
|
} catch (err) {
|
|
1311
453
|
if (err.code === "ENOENT") {
|
|
1312
454
|
let available = "";
|
|
@@ -1323,24 +465,20 @@ function readFileBytesOrThrow(filePath) {
|
|
|
1323
465
|
}
|
|
1324
466
|
var DIST_DIR = import.meta.dirname;
|
|
1325
467
|
function getAssetContent(folder, filename, fallbackMessage) {
|
|
1326
|
-
const filePath =
|
|
1327
|
-
if (
|
|
1328
|
-
return
|
|
468
|
+
const filePath = join(DIST_DIR, folder, filename);
|
|
469
|
+
if (existsSync(filePath)) {
|
|
470
|
+
return readFileSync(filePath, "utf-8");
|
|
1329
471
|
}
|
|
1330
472
|
return fallbackMessage;
|
|
1331
473
|
}
|
|
1332
474
|
var READ_DOCX_COMMON_DESC = "Reads a DOCX file. Returns text with inline CriticMarkup for Tracked Changes and Comments: {++inserted++}, {--deleted--}, {==highlighted==}{>>comment<<}. Set clean_view=True for the finalized 'Accepted' text without markup.\n\n";
|
|
1333
475
|
var READ_DOCX_TAIL = "Modes:\n- 'full' (default): paginated body content. Use page=N to navigate.\n- 'outline': heading map only \u2014 start here for large docs to plan targeted reads. Defaults to L1-L2 headings; pass outline_max_level=3-6 to see deeper structure.\n- 'appendix': defined terms, anchors, and cross-reference targets. Consult before editing legal/technical docs to avoid breaking references.";
|
|
1334
|
-
var PROCESS_BATCH_COMMON_DESC = "Applies a batch of edits and review actions to a DOCX.\n\
|
|
476
|
+
var PROCESS_BATCH_COMMON_DESC = "Applies a batch of edits and review actions to a DOCX.\n\nBatches apply SEQUENTIALLY: each change is validated and applied against the document state produced by the changes before it, so you may chain dependent edits within one batch (e.g. rename X to Y, then modify Y \u2014 the second edit must target Y, the text as it reads after the rename). Validation failures reject the whole batch transactionally: nothing is applied until every change resolves.\n\n";
|
|
1335
477
|
var PROCESS_BATCH_OPERATIONS_DESC = "Each item in `changes` must specify a `type`:\n1. 'modify': Search-and-replace. By default `target_text` must match uniquely (`match_mode`:'strict') \u2014 add surrounding context to disambiguate, or set `match_mode`:'first'/'all' to edit the first or every occurrence. Set `regex`:true to treat `target_text` as a regular expression (capture groups available in `new_text` as $1, $2\u2026). `new_text` supports Markdown: '# Heading 1' through '###### Heading 6', '**bold**', '_italic_', and '\\n\\n' to split into multiple paragraphs. Empty `new_text` deletes. Do NOT write CriticMarkup tags ({++, {--, {>>) manually \u2014 use the `comment` parameter for comments.\n \u2022 EMPTY/FORM TABLE CELLS: a blank cell has no text to match. `read_docx` renders each cell with a trailing `{#cell:<id>}` anchor \u2014 to fill a blank cell, set `target_text` to that exact anchor (e.g. '{#cell:0000005E}') and put the value in `new_text`. Do NOT try to match the pipe layout ('Date | | |'); the pipes are display separators, not editable text.\n2. 'accept' / 'reject': Finalize or revert a tracked change by `target_id` (e.g. 'Chg:12').\n3. 'reply': Reply to a comment by `target_id` (e.g. 'Com:5') with `text`.\n4. 'insert_row' / 'delete_row': Table edits. Disk mode only \u2014 not supported on Live Word canvas.\n\nID VOLATILITY: 'Chg:N' and 'Com:N' shift between document states. Always call `read_docx` immediately before any accept/reject/reply \u2014 do not reuse IDs from earlier in the conversation. The `{#cell:<id>}` anchors are stable (Word-assigned) and safe to reuse across reads.\n\n`author_name` is used for attribution on all tracked changes and comments, in both disk and Live Word modes.";
|
|
1336
478
|
var DIFF_DOCX_DESC = "Compares two DOCX files and returns a compact `@@ Word Patch @@` diff \u2014 Adeu's token-level, sub-word patch format \u2014 of their text content. Useful for analyzing differences between versions before editing.";
|
|
1337
|
-
var gitSha = "
|
|
1338
|
-
var packageVersion = "1.
|
|
479
|
+
var gitSha = "4947f97";
|
|
480
|
+
var packageVersion = "1.22.0";
|
|
1339
481
|
var buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
|
|
1340
|
-
var args = process.argv.slice(2);
|
|
1341
|
-
var scopeIdx = args.indexOf("--scope");
|
|
1342
|
-
var requestedScope = (scopeIdx !== -1 ? args[scopeIdx + 1] : "all").toLowerCase();
|
|
1343
|
-
var isDocxOnly = requestedScope === "docx";
|
|
1344
482
|
var server = new McpServer({
|
|
1345
483
|
name: "adeu-redlining-service",
|
|
1346
484
|
version: packageVersion
|
|
@@ -1399,31 +537,6 @@ registerAppResource(
|
|
|
1399
537
|
};
|
|
1400
538
|
}
|
|
1401
539
|
);
|
|
1402
|
-
registerAppResource(
|
|
1403
|
-
server,
|
|
1404
|
-
EMAIL_UI_URI,
|
|
1405
|
-
EMAIL_UI_URI,
|
|
1406
|
-
{ mimeType: RESOURCE_MIME_TYPE, description: "Adeu Email Viewer UI" },
|
|
1407
|
-
async () => {
|
|
1408
|
-
let html = getAssetContent(
|
|
1409
|
-
"templates",
|
|
1410
|
-
"email_ui.html",
|
|
1411
|
-
"<html><body>UI Template Not Found</body></html>"
|
|
1412
|
-
);
|
|
1413
|
-
const svg = getAssetContent("assets", "adeu.svg", "");
|
|
1414
|
-
html = html.replace("[[ adeu_svg_code ]]", svg);
|
|
1415
|
-
return {
|
|
1416
|
-
contents: [
|
|
1417
|
-
{
|
|
1418
|
-
uri: EMAIL_UI_URI,
|
|
1419
|
-
mimeType: RESOURCE_MIME_TYPE,
|
|
1420
|
-
text: html,
|
|
1421
|
-
_meta: { ui: { csp: UI_CSP } }
|
|
1422
|
-
}
|
|
1423
|
-
]
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
);
|
|
1427
540
|
registerAppTool(
|
|
1428
541
|
server,
|
|
1429
542
|
"read_docx",
|
|
@@ -1442,7 +555,7 @@ registerAppTool(
|
|
|
1442
555
|
"'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms."
|
|
1443
556
|
),
|
|
1444
557
|
page: z.union([z.number(), z.string()]).optional().describe(
|
|
1445
|
-
"Without `search_query`: 1-indexed document page to display (defaults to 1). With `search_query`: restricts matches to that document page (defaults to searching all pages; pass `page='all'` to be explicit)."
|
|
558
|
+
"Without `search_query`: 1-indexed document page to display (defaults to 1). With `search_query`: restricts matches to that document page (defaults to searching all pages; pass `page='all'` to be explicit). Note: pages are synthetic, length-based content chunks sized for LLM consumption \u2014 they do NOT correspond to printed Word pages or explicit page breaks."
|
|
1446
559
|
),
|
|
1447
560
|
outline_max_level: z.coerce.number().default(2).describe("For mode='outline' only: cap on heading depth."),
|
|
1448
561
|
outline_verbose: z.boolean().default(false).describe("For mode='outline' only: includes metadata."),
|
|
@@ -1501,7 +614,26 @@ registerAppTool(
|
|
|
1501
614
|
);
|
|
1502
615
|
return res2;
|
|
1503
616
|
}
|
|
1504
|
-
|
|
617
|
+
if (mode === "full" && page !== void 0 && page !== null && String(page).trim().toLowerCase() === "all") {
|
|
618
|
+
const res2 = build_full_document_response(text, file_path);
|
|
619
|
+
return res2;
|
|
620
|
+
}
|
|
621
|
+
let resolvedPage = 1;
|
|
622
|
+
if (page !== void 0 && page !== null) {
|
|
623
|
+
const parsed = typeof page === "number" ? page : parseInt(String(page).trim(), 10);
|
|
624
|
+
if (!Number.isFinite(parsed)) {
|
|
625
|
+
return {
|
|
626
|
+
isError: true,
|
|
627
|
+
content: [
|
|
628
|
+
{
|
|
629
|
+
type: "text",
|
|
630
|
+
text: `Invalid page value: '${page}'. Provide a positive integer (pages are 1-indexed; 'all' is valid for mode='full' and together with search_query).`
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
resolvedPage = parsed;
|
|
636
|
+
}
|
|
1505
637
|
if (mode === "appendix") {
|
|
1506
638
|
const res2 = build_appendix_response(text, resolvedPage, file_path);
|
|
1507
639
|
return res2;
|
|
@@ -1560,7 +692,7 @@ server.registerTool(
|
|
|
1560
692
|
original_docx_path: z.string().describe("Absolute path to the source file."),
|
|
1561
693
|
author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
|
|
1562
694
|
changes: z.array(z.union([z.string(), CHANGE_ITEM_SCHEMA])).describe(
|
|
1563
|
-
"Ordered list of changes to apply. Each item is an object carrying a `type` discriminator plus that type's fields (see the per-field docs and the tool description).
|
|
695
|
+
"Ordered list of changes to apply. Each item is an object carrying a `type` discriminator plus that type's fields (see the per-field docs and the tool description). Items apply SEQUENTIALLY: each one evaluates against the document state produced by the items before it, so later items may target text an earlier item introduced."
|
|
1564
696
|
),
|
|
1565
697
|
output_path: z.string().optional().describe("Optional output path."),
|
|
1566
698
|
dry_run: z.boolean().optional().default(false).describe(
|
|
@@ -1584,6 +716,16 @@ server.registerTool(
|
|
|
1584
716
|
{ type: "text", text: "Error: author_name cannot be empty." }
|
|
1585
717
|
]
|
|
1586
718
|
};
|
|
719
|
+
const author_ctrl = describe_illegal_control_chars(author_name);
|
|
720
|
+
if (author_ctrl)
|
|
721
|
+
return {
|
|
722
|
+
content: [
|
|
723
|
+
{
|
|
724
|
+
type: "text",
|
|
725
|
+
text: `Error: author_name contains control character(s) (${author_ctrl}) that cannot be stored in a DOCX. Remove them and retry.`
|
|
726
|
+
}
|
|
727
|
+
]
|
|
728
|
+
};
|
|
1587
729
|
if (!changes || changes.length === 0)
|
|
1588
730
|
return {
|
|
1589
731
|
content: [{ type: "text", text: "Error: No changes provided." }]
|
|
@@ -1667,9 +809,26 @@ ${e.errors.join("\n\n")}`
|
|
|
1667
809
|
}
|
|
1668
810
|
if (!dry_run) {
|
|
1669
811
|
const outBuf = await doc.save();
|
|
1670
|
-
|
|
812
|
+
try {
|
|
813
|
+
fs.writeFileSync(outPath, outBuf);
|
|
814
|
+
} catch (e) {
|
|
815
|
+
return {
|
|
816
|
+
isError: true,
|
|
817
|
+
content: [
|
|
818
|
+
{
|
|
819
|
+
type: "text",
|
|
820
|
+
text: `Could not write output file '${outPath}': ${e.message}`
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
let res = formatBatchResult(stats, outPath, !!dry_run);
|
|
827
|
+
if (sanitizedChanges.length === 0) {
|
|
828
|
+
res = `\u26A0\uFE0F 0 changes provided \u2014 nothing to do. The output is an unmodified copy of the original.
|
|
829
|
+
|
|
830
|
+
` + res;
|
|
1671
831
|
}
|
|
1672
|
-
const res = formatBatchResult(stats, outPath, !!dry_run);
|
|
1673
832
|
return { content: [{ type: "text", text: res }] };
|
|
1674
833
|
} catch (e) {
|
|
1675
834
|
return {
|
|
@@ -1829,130 +988,6 @@ ${result.reportText}`
|
|
|
1829
988
|
}
|
|
1830
989
|
}
|
|
1831
990
|
);
|
|
1832
|
-
if (!isDocxOnly) {
|
|
1833
|
-
registerAppTool(
|
|
1834
|
-
server,
|
|
1835
|
-
"search_and_fetch_emails",
|
|
1836
|
-
{
|
|
1837
|
-
title: "Search & Fetch Emails",
|
|
1838
|
-
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. The mailbox used in the original search is remembered with the short ID and re-applied automatically when you fetch or reply without specifying `mailbox_address`.\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; the directory is created automatically if it does not exist. This directory path refers to the user's native operating system, not the LLM's sandbox environment.",
|
|
1839
|
-
inputSchema: z.object({
|
|
1840
|
-
reasoning: z.string().describe(
|
|
1841
|
-
"Why do I need to search or fetch these emails? State this reason before any other parameter."
|
|
1842
|
-
),
|
|
1843
|
-
sender: z.string().optional(),
|
|
1844
|
-
subject: z.string().optional(),
|
|
1845
|
-
has_attachments: z.boolean().optional(),
|
|
1846
|
-
attachment_name: z.string().optional(),
|
|
1847
|
-
is_unread: z.boolean().optional(),
|
|
1848
|
-
days_ago: z.coerce.number().optional(),
|
|
1849
|
-
folder: z.enum(["inbox", "sent", "all"]).optional(),
|
|
1850
|
-
limit: z.coerce.number().default(10),
|
|
1851
|
-
offset: z.coerce.number().default(0),
|
|
1852
|
-
email_id: z.string().optional(),
|
|
1853
|
-
working_directory: z.string().optional().describe(
|
|
1854
|
-
"Optional. The current working directory of the project or task. If provided, attachments will be saved here under an 'adeu_attachments' subfolder; the directory is created automatically if it does not exist. If omitted, attachments are saved to the system temp directory."
|
|
1855
|
-
),
|
|
1856
|
-
mailbox_address: z.string().optional().describe("Optional target mailbox email address to search within."),
|
|
1857
|
-
task_id: z.string().optional().describe("If resuming a pending check, provide the task ID here."),
|
|
1858
|
-
max_attachment_size_mb: z.coerce.number().optional().describe(
|
|
1859
|
-
"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."
|
|
1860
|
-
)
|
|
1861
|
-
}),
|
|
1862
|
-
_meta: { ui: { resourceUri: EMAIL_UI_URI } }
|
|
1863
|
-
},
|
|
1864
|
-
async (args2) => {
|
|
1865
|
-
try {
|
|
1866
|
-
return await search_and_fetch_emails(args2);
|
|
1867
|
-
} catch (e) {
|
|
1868
|
-
return {
|
|
1869
|
-
isError: true,
|
|
1870
|
-
content: [{ type: "text", text: e.message }]
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
);
|
|
1875
|
-
server.registerTool(
|
|
1876
|
-
"login_to_adeu_cloud",
|
|
1877
|
-
{
|
|
1878
|
-
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.",
|
|
1879
|
-
inputSchema: {
|
|
1880
|
-
reasoning: z.string().describe(
|
|
1881
|
-
"Why do I need to log in to Adeu Cloud? State this reason before any other parameter."
|
|
1882
|
-
)
|
|
1883
|
-
}
|
|
1884
|
-
},
|
|
1885
|
-
async () => {
|
|
1886
|
-
try {
|
|
1887
|
-
return await login_to_adeu_cloud();
|
|
1888
|
-
} catch (e) {
|
|
1889
|
-
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
);
|
|
1893
|
-
server.registerTool(
|
|
1894
|
-
"logout_of_adeu_cloud",
|
|
1895
|
-
{
|
|
1896
|
-
description: "Logs out of the Adeu Cloud backend.",
|
|
1897
|
-
inputSchema: {
|
|
1898
|
-
reasoning: z.string().describe(
|
|
1899
|
-
"Why do I need to log out of Adeu Cloud? State this reason before any other parameter."
|
|
1900
|
-
)
|
|
1901
|
-
}
|
|
1902
|
-
},
|
|
1903
|
-
async () => {
|
|
1904
|
-
try {
|
|
1905
|
-
return await logout_of_adeu_cloud();
|
|
1906
|
-
} catch (e) {
|
|
1907
|
-
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
);
|
|
1911
|
-
server.registerTool(
|
|
1912
|
-
"create_email_draft",
|
|
1913
|
-
{
|
|
1914
|
-
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.",
|
|
1915
|
-
inputSchema: {
|
|
1916
|
-
reasoning: z.string().describe(
|
|
1917
|
-
"Why do I need to create this email draft? State this reason before any other parameter."
|
|
1918
|
-
),
|
|
1919
|
-
body_markdown: z.string(),
|
|
1920
|
-
reply_to_email_id: z.string().optional(),
|
|
1921
|
-
subject: z.string().optional(),
|
|
1922
|
-
to_recipients: z.array(z.string()).optional(),
|
|
1923
|
-
attachment_paths: z.array(z.string()).optional(),
|
|
1924
|
-
mailbox_address: z.string().optional().describe(
|
|
1925
|
-
"Optional target mailbox email address to create the draft in."
|
|
1926
|
-
)
|
|
1927
|
-
}
|
|
1928
|
-
},
|
|
1929
|
-
async (args2) => {
|
|
1930
|
-
try {
|
|
1931
|
-
return await create_email_draft(args2);
|
|
1932
|
-
} catch (e) {
|
|
1933
|
-
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
);
|
|
1937
|
-
server.registerTool(
|
|
1938
|
-
"list_available_mailboxes",
|
|
1939
|
-
{
|
|
1940
|
-
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.",
|
|
1941
|
-
inputSchema: {
|
|
1942
|
-
reasoning: z.string().describe(
|
|
1943
|
-
"Why do I need to list available mailboxes? State this reason before any other parameter."
|
|
1944
|
-
)
|
|
1945
|
-
}
|
|
1946
|
-
},
|
|
1947
|
-
async () => {
|
|
1948
|
-
try {
|
|
1949
|
-
return await list_available_mailboxes();
|
|
1950
|
-
} catch (e) {
|
|
1951
|
-
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
);
|
|
1955
|
-
}
|
|
1956
991
|
function formatBatchResult(stats, outPath, dry_run) {
|
|
1957
992
|
let res = "";
|
|
1958
993
|
if (dry_run) {
|
|
@@ -2018,8 +1053,8 @@ ${stats.skipped_details.join("\n")}`;
|
|
|
2018
1053
|
async function main() {
|
|
2019
1054
|
const transport = new StdioServerTransport();
|
|
2020
1055
|
await server.connect(transport);
|
|
2021
|
-
const gitSha2 = "
|
|
2022
|
-
const buildTs = "2026-07-
|
|
1056
|
+
const gitSha2 = "4947f97";
|
|
1057
|
+
const buildTs = "2026-07-18T07:52:19.202Z";
|
|
2023
1058
|
console.error(
|
|
2024
1059
|
`Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha2}@${buildTs}`
|
|
2025
1060
|
);
|