@adeu/mcp-server 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/adeu.svg +3 -0
- package/dist/assets/logo.png +0 -0
- package/dist/assets/marked.min.js +69 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +904 -447
- package/dist/index.js.map +1 -1
- package/dist/templates/email_ui.html +667 -0
- package/dist/templates/markdown_ui.html +745 -0
- package/package.json +4 -2
- package/src/assets/adeu.svg +3 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/marked.min.js +69 -0
- package/src/formatter.test.ts +64 -0
- package/src/index.ts +577 -407
- package/src/mcp.cloud.test.ts +13 -0
- package/src/response-builders.ts +111 -50
- package/src/templates/email_ui.html +667 -0
- package/src/templates/markdown_ui.html +745 -0
- package/src/tools/auth.ts +1 -0
- package/src/tools/email.test.ts +258 -0
- package/src/tools/email.ts +491 -54
- package/tsup.config.ts +35 -11
package/dist/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
7
|
+
import { basename as basename2, resolve as resolve2, extname, dirname, join as join3 } from "path";
|
|
8
|
+
import { z } from "zod";
|
|
6
9
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import
|
|
10
|
+
registerAppTool,
|
|
11
|
+
registerAppResource,
|
|
12
|
+
RESOURCE_MIME_TYPE
|
|
13
|
+
} from "@modelcontextprotocol/ext-apps/server";
|
|
14
|
+
import fs from "fs";
|
|
12
15
|
import {
|
|
13
16
|
identifyEngine,
|
|
14
17
|
extractTextFromBuffer,
|
|
@@ -20,7 +23,7 @@ import {
|
|
|
20
23
|
} from "@adeu/core";
|
|
21
24
|
|
|
22
25
|
// src/response-builders.ts
|
|
23
|
-
import { resolve } from "path";
|
|
26
|
+
import { resolve, basename } from "path";
|
|
24
27
|
import {
|
|
25
28
|
paginate,
|
|
26
29
|
split_structural_appendix,
|
|
@@ -66,7 +69,8 @@ Document has ${nodes.length} headings, all at deeper levels. Call read_docx with
|
|
|
66
69
|
if (verbose) {
|
|
67
70
|
const meta_parts = [`p${node.page}`, node.style];
|
|
68
71
|
if (node.has_table) meta_parts.push("has table");
|
|
69
|
-
if (node.footnote_ids && node.footnote_ids.length > 0)
|
|
72
|
+
if (node.footnote_ids && node.footnote_ids.length > 0)
|
|
73
|
+
meta_parts.push("fn:" + node.footnote_ids.join(","));
|
|
70
74
|
lines.push(`${prefix} ${node.text} (${meta_parts.join(", ")})`);
|
|
71
75
|
} else {
|
|
72
76
|
lines.push(`${prefix} ${node.text} (p${node.page})`);
|
|
@@ -79,18 +83,30 @@ function build_paginated_response(text, page, file_path) {
|
|
|
79
83
|
const has_appendix = Boolean(appendix.trim());
|
|
80
84
|
const result = paginate(body, "");
|
|
81
85
|
if (page < 1 || page > result.total_pages) {
|
|
82
|
-
throw new Error(
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Page ${page} out of range (doc has ${result.total_pages} pages).`
|
|
88
|
+
);
|
|
83
89
|
}
|
|
84
90
|
const selected = result.pages[page - 1];
|
|
85
91
|
const banner = _build_page_banner(selected.page, selected.total_pages);
|
|
86
|
-
const footer = _build_page_footer(
|
|
92
|
+
const footer = _build_page_footer(
|
|
93
|
+
selected.page,
|
|
94
|
+
selected.total_pages,
|
|
95
|
+
selected.has_next
|
|
96
|
+
);
|
|
87
97
|
const appendix_pointer = _build_appendix_pointer(has_appendix);
|
|
88
98
|
const ui_markdown = banner + selected.page_content + footer + appendix_pointer;
|
|
89
99
|
const llm_content = `> **File Path:** \`${resolve(file_path)}\`
|
|
90
100
|
|
|
91
101
|
${ui_markdown}`;
|
|
92
102
|
return {
|
|
93
|
-
content: [{ type: "text", text: llm_content }]
|
|
103
|
+
content: [{ type: "text", text: llm_content }],
|
|
104
|
+
// Include structuredContent for the UI to render the markdown
|
|
105
|
+
structuredContent: {
|
|
106
|
+
markdown: ui_markdown,
|
|
107
|
+
file_path: resolve(file_path),
|
|
108
|
+
title: basename(file_path)
|
|
109
|
+
}
|
|
94
110
|
};
|
|
95
111
|
}
|
|
96
112
|
function build_outline_response(doc, projected_text, file_path, outline_max_level = 2, outline_verbose = false) {
|
|
@@ -102,8 +118,14 @@ function build_outline_response(doc, projected_text, file_path, outline_max_leve
|
|
|
102
118
|
pagination_result.body_pages,
|
|
103
119
|
pagination_result.body_page_offsets
|
|
104
120
|
);
|
|
105
|
-
const rendered = render_outline_tree(
|
|
106
|
-
|
|
121
|
+
const rendered = render_outline_tree(
|
|
122
|
+
nodes,
|
|
123
|
+
outline_max_level,
|
|
124
|
+
outline_verbose
|
|
125
|
+
);
|
|
126
|
+
const visible_count = nodes.filter(
|
|
127
|
+
(n) => n.level <= outline_max_level
|
|
128
|
+
).length;
|
|
107
129
|
const deeper_count = nodes.length - visible_count;
|
|
108
130
|
const deeper_hint = deeper_count > 0 ? ` (${deeper_count} more at deeper levels, raise outline_max_level to see)` : "";
|
|
109
131
|
const header = `> **Outline view** \u2014 showing ${visible_count} of ${nodes.length} headings (L1-L${outline_max_level}${deeper_hint}) across ${pagination_result.total_pages} page(s). Call \`read_docx\` with \`mode='full'\` and \`page=N\` to read a section.
|
|
@@ -116,7 +138,12 @@ function build_outline_response(doc, projected_text, file_path, outline_max_leve
|
|
|
116
138
|
|
|
117
139
|
${ui_markdown}`;
|
|
118
140
|
return {
|
|
119
|
-
content: [{ type: "text", text: llm_content }]
|
|
141
|
+
content: [{ type: "text", text: llm_content }],
|
|
142
|
+
structuredContent: {
|
|
143
|
+
markdown: ui_markdown,
|
|
144
|
+
file_path: resolve(file_path),
|
|
145
|
+
title: `Outline: ${basename(file_path)}`
|
|
146
|
+
}
|
|
120
147
|
};
|
|
121
148
|
}
|
|
122
149
|
function build_appendix_response(text, page, file_path) {
|
|
@@ -127,12 +154,19 @@ function build_appendix_response(text, page, file_path) {
|
|
|
127
154
|
|
|
128
155
|
${ui_markdown2}`;
|
|
129
156
|
return {
|
|
130
|
-
content: [{ type: "text", text: llm_content2 }]
|
|
157
|
+
content: [{ type: "text", text: llm_content2 }],
|
|
158
|
+
structuredContent: {
|
|
159
|
+
markdown: ui_markdown2,
|
|
160
|
+
file_path: resolve(file_path),
|
|
161
|
+
title: `Appendix: ${basename(file_path)}`
|
|
162
|
+
}
|
|
131
163
|
};
|
|
132
164
|
}
|
|
133
165
|
const result = paginate(appendix, "");
|
|
134
166
|
if (page < 1 || page > result.total_pages) {
|
|
135
|
-
throw new Error(
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Appendix page ${page} out of range (appendix has ${result.total_pages} pages).`
|
|
169
|
+
);
|
|
136
170
|
}
|
|
137
171
|
const selected = result.pages[page - 1];
|
|
138
172
|
let banner = "";
|
|
@@ -156,7 +190,12 @@ ${ui_markdown2}`;
|
|
|
156
190
|
|
|
157
191
|
${ui_markdown}`;
|
|
158
192
|
return {
|
|
159
|
-
content: [{ type: "text", text: llm_content }]
|
|
193
|
+
content: [{ type: "text", text: llm_content }],
|
|
194
|
+
structuredContent: {
|
|
195
|
+
markdown: ui_markdown,
|
|
196
|
+
file_path: resolve(file_path),
|
|
197
|
+
title: `Appendix: ${basename(file_path)}`
|
|
198
|
+
}
|
|
160
199
|
};
|
|
161
200
|
}
|
|
162
201
|
|
|
@@ -177,6 +216,8 @@ import {
|
|
|
177
216
|
// src/shared.ts
|
|
178
217
|
var FRONTEND_URL = process.env.ADEU_FRONTEND_URL || "https://app.adeu.ai";
|
|
179
218
|
var BACKEND_URL = process.env.ADEU_BACKEND_URL || "https://app.adeu.ai";
|
|
219
|
+
var MARKDOWN_UI_URI = "ui://adeu/markdown-ui";
|
|
220
|
+
var EMAIL_UI_URI = "ui://adeu/email-ui";
|
|
180
221
|
|
|
181
222
|
// src/desktop-auth.ts
|
|
182
223
|
var ADEU_DIR = join(homedir(), ".adeu");
|
|
@@ -279,7 +320,8 @@ async function login_to_adeu_cloud() {
|
|
|
279
320
|
headers: {
|
|
280
321
|
Authorization: `Bearer ${apiKey}`,
|
|
281
322
|
Accept: "application/json"
|
|
282
|
-
}
|
|
323
|
+
},
|
|
324
|
+
signal: AbortSignal.timeout(15e3)
|
|
283
325
|
});
|
|
284
326
|
if (res.status === 401) {
|
|
285
327
|
DesktopAuthManager.clearApiKey();
|
|
@@ -318,8 +360,41 @@ import { homedir as homedir2, tmpdir } from "os";
|
|
|
318
360
|
import { join as join2 } from "path";
|
|
319
361
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
320
362
|
import { createHash } from "crypto";
|
|
363
|
+
var KNOWN_ERROR_HINTS = {
|
|
364
|
+
"Email not found.": "The email ID was not found. If this was a short ID (msg_*), it may have been evicted from the local cache or come from a different machine \u2014 re-run search_and_fetch_emails with filters to get a fresh ID. If it was an adeu_<numeric> or raw provider ID, verify it's correct.",
|
|
365
|
+
"Adeu email reference not found.": "The adeu_<id> reference doesn't resolve to any processed email for this user. Verify the ID, or re-run search_and_fetch_emails with filters to find the message.",
|
|
366
|
+
"Invalid adeu_ email ID format.": "The adeu_<id> reference is malformed. Expected format: adeu_<integer>."
|
|
367
|
+
};
|
|
368
|
+
function formatBackendError(statusCode, responseBody) {
|
|
369
|
+
let detail = responseBody;
|
|
370
|
+
try {
|
|
371
|
+
const parsed = JSON.parse(responseBody);
|
|
372
|
+
if (parsed && typeof parsed === "object" && "detail" in parsed) {
|
|
373
|
+
detail = String(parsed.detail);
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
let hint = KNOWN_ERROR_HINTS[detail];
|
|
378
|
+
if (!hint && detail.startsWith("Mailbox '") && detail.endsWith("' not found.")) {
|
|
379
|
+
const mailbox = detail.slice("Mailbox '".length, -"' not found.".length);
|
|
380
|
+
hint = `The mailbox '${mailbox}' is not connected to your Adeu account. Call list_available_mailboxes to see valid mailbox addresses, then retry with one of those as \`mailbox_address\`.`;
|
|
381
|
+
}
|
|
382
|
+
const message = hint ?? detail;
|
|
383
|
+
return `Cloud search failed (HTTP ${statusCode}): ${message}`;
|
|
384
|
+
}
|
|
385
|
+
function isTimeoutError(err) {
|
|
386
|
+
if (!err || typeof err !== "object") return false;
|
|
387
|
+
const name = err.name;
|
|
388
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
389
|
+
}
|
|
321
390
|
var CACHE_FILE = join2(homedir2(), ".adeu", "mcp_id_cache.json");
|
|
322
391
|
var MAX_CACHE_SIZE = 1e3;
|
|
392
|
+
function formatBytes(bytes) {
|
|
393
|
+
if (bytes == null) return "unknown size";
|
|
394
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
395
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
396
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
397
|
+
}
|
|
323
398
|
function loadIdCache() {
|
|
324
399
|
if (existsSync2(CACHE_FILE)) {
|
|
325
400
|
try {
|
|
@@ -350,32 +425,173 @@ function minifyEmailId(realId, cache) {
|
|
|
350
425
|
cache[shortId] = realId;
|
|
351
426
|
return shortId;
|
|
352
427
|
}
|
|
428
|
+
var StaleShortIdError = class extends Error {
|
|
429
|
+
constructor(shortId) {
|
|
430
|
+
super(
|
|
431
|
+
`Short ID '${shortId}' is not in the local cache (it may have been evicted, or it came from a different machine/session). Short IDs only persist on the machine where they were generated. Re-run search_and_fetch_emails with filters (sender, subject, days_ago) to fetch fresh IDs, then use the new ID from those results.`
|
|
432
|
+
);
|
|
433
|
+
this.name = "StaleShortIdError";
|
|
434
|
+
}
|
|
435
|
+
};
|
|
353
436
|
function resolveEmailId(shortId) {
|
|
354
437
|
if (!shortId) return shortId;
|
|
438
|
+
if (shortId.startsWith("adeu_")) return shortId;
|
|
355
439
|
const cache = loadIdCache();
|
|
356
|
-
|
|
440
|
+
const resolved = cache[shortId];
|
|
441
|
+
if (resolved) return resolved;
|
|
442
|
+
if (shortId.startsWith("msg_")) {
|
|
443
|
+
throw new StaleShortIdError(shortId);
|
|
444
|
+
}
|
|
445
|
+
return shortId;
|
|
446
|
+
}
|
|
447
|
+
var HTML_NAMED_ENTITIES = {
|
|
448
|
+
nbsp: " ",
|
|
449
|
+
amp: "&",
|
|
450
|
+
lt: "<",
|
|
451
|
+
gt: ">",
|
|
452
|
+
quot: '"',
|
|
453
|
+
apos: "'",
|
|
454
|
+
copy: "\xA9",
|
|
455
|
+
reg: "\xAE",
|
|
456
|
+
trade: "\u2122",
|
|
457
|
+
hellip: "\u2026",
|
|
458
|
+
mdash: "\u2014",
|
|
459
|
+
ndash: "\u2013",
|
|
460
|
+
lsquo: "\u2018",
|
|
461
|
+
rsquo: "\u2019",
|
|
462
|
+
ldquo: "\u201C",
|
|
463
|
+
rdquo: "\u201D",
|
|
464
|
+
laquo: "\xAB",
|
|
465
|
+
raquo: "\xBB",
|
|
466
|
+
bull: "\u2022",
|
|
467
|
+
middot: "\xB7",
|
|
468
|
+
deg: "\xB0",
|
|
469
|
+
plusmn: "\xB1",
|
|
470
|
+
times: "\xD7",
|
|
471
|
+
divide: "\xF7",
|
|
472
|
+
euro: "\u20AC",
|
|
473
|
+
pound: "\xA3",
|
|
474
|
+
yen: "\xA5",
|
|
475
|
+
cent: "\xA2",
|
|
476
|
+
sect: "\xA7",
|
|
477
|
+
para: "\xB6",
|
|
478
|
+
iexcl: "\xA1",
|
|
479
|
+
iquest: "\xBF"
|
|
480
|
+
};
|
|
481
|
+
function decodeHtmlEntities(text) {
|
|
482
|
+
text = text.replace(/&#(\d+);/g, (_, dec) => {
|
|
483
|
+
const code = parseInt(dec, 10);
|
|
484
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : _;
|
|
485
|
+
});
|
|
486
|
+
text = text.replace(/&#[xX]([0-9a-fA-F]+);/g, (_, hex) => {
|
|
487
|
+
const code = parseInt(hex, 16);
|
|
488
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : _;
|
|
489
|
+
});
|
|
490
|
+
text = text.replace(/&([a-zA-Z][a-zA-Z0-9]*);/g, (match, name) => {
|
|
491
|
+
const replacement = HTML_NAMED_ENTITIES[name.toLowerCase()];
|
|
492
|
+
return replacement !== void 0 ? replacement : match;
|
|
493
|
+
});
|
|
494
|
+
return text;
|
|
357
495
|
}
|
|
358
496
|
function stripTags(html) {
|
|
359
497
|
if (!html) return "";
|
|
360
|
-
let text = html
|
|
498
|
+
let text = html;
|
|
499
|
+
const suppressPattern = /<(style|script|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
500
|
+
let prev;
|
|
501
|
+
do {
|
|
502
|
+
prev = text;
|
|
503
|
+
text = text.replace(suppressPattern, "");
|
|
504
|
+
} while (text !== prev);
|
|
505
|
+
text = text.replace(/<(style|script|head|title)\b[^>]*>[\s\S]*$/gi, "");
|
|
361
506
|
text = text.replace(
|
|
362
507
|
/<\/?(p|div|br|hr|tr|li|h[1-6]|blockquote)\b[^>]*>/gi,
|
|
363
508
|
"\n"
|
|
364
509
|
);
|
|
365
510
|
text = text.replace(/<[^>]+>/g, "");
|
|
511
|
+
text = decodeHtmlEntities(text);
|
|
366
512
|
return text.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
|
|
367
513
|
}
|
|
368
514
|
function removeNestedQuotes(text) {
|
|
369
515
|
if (!text) return "";
|
|
370
|
-
const
|
|
516
|
+
const fromTokens = [
|
|
517
|
+
"From",
|
|
518
|
+
// English
|
|
519
|
+
"L\xE4hett\xE4j\xE4",
|
|
520
|
+
// Finnish
|
|
521
|
+
"Fr\xE5n",
|
|
522
|
+
// Swedish
|
|
523
|
+
"Von",
|
|
524
|
+
// German
|
|
525
|
+
"De",
|
|
526
|
+
// French / Spanish / Portuguese
|
|
527
|
+
"Da",
|
|
528
|
+
// Italian
|
|
529
|
+
"Van",
|
|
530
|
+
// Dutch
|
|
531
|
+
"Fra",
|
|
532
|
+
// Norwegian / Danish
|
|
533
|
+
"Mittente"
|
|
534
|
+
// Italian (alt)
|
|
535
|
+
];
|
|
536
|
+
const sentTokens = [
|
|
537
|
+
"Sent",
|
|
538
|
+
"L\xE4hetetty",
|
|
539
|
+
"Skickat",
|
|
540
|
+
"Gesendet",
|
|
541
|
+
"Envoy\xE9",
|
|
542
|
+
"Enviado",
|
|
543
|
+
"Inviato",
|
|
544
|
+
"Verzonden",
|
|
545
|
+
"Sendt"
|
|
546
|
+
];
|
|
547
|
+
const wrotePatterns = [
|
|
548
|
+
/On .{1,200}? wrote:/,
|
|
549
|
+
// English
|
|
550
|
+
/Le .{1,200}? a écrit\s*:/i,
|
|
551
|
+
// French
|
|
552
|
+
/Am .{1,200}? schrieb .{1,100}?:/i,
|
|
553
|
+
// German
|
|
554
|
+
/El .{1,200}? escribió\s*:/i,
|
|
555
|
+
// Spanish
|
|
556
|
+
/Il .{1,200}? ha scritto\s*:/i,
|
|
557
|
+
// Italian
|
|
558
|
+
/Op .{1,200}? schreef .{1,100}?:/i,
|
|
559
|
+
// Dutch
|
|
560
|
+
/Den .{1,200}? skrev .{1,100}?:/i,
|
|
561
|
+
// Swedish/Norwegian/Danish
|
|
562
|
+
/Em .{1,200}? escreveu\s*:/i,
|
|
563
|
+
// Portuguese
|
|
564
|
+
/Em\b.{1,200}?, .{1,200}? escreveu\s*:/i,
|
|
565
|
+
// Portuguese (date prefix)
|
|
566
|
+
new RegExp(
|
|
567
|
+
`^(${fromTokens.join("|")})\\s*:.*?\\n(?:.*\\n){0,5}?(${sentTokens.join("|")})\\s*:`,
|
|
568
|
+
"m"
|
|
569
|
+
)
|
|
570
|
+
];
|
|
571
|
+
const forwardedTokens = [
|
|
572
|
+
"Forwarded message",
|
|
573
|
+
"V\xE4litetty viesti",
|
|
574
|
+
"Vidarebefordrat meddelande",
|
|
575
|
+
"Weitergeleitete Nachricht",
|
|
576
|
+
"Message transf\xE9r\xE9",
|
|
577
|
+
"Mensaje reenviado",
|
|
578
|
+
"Messaggio inoltrato",
|
|
579
|
+
"Doorgestuurd bericht",
|
|
580
|
+
"Videresendt melding",
|
|
581
|
+
"Videresendt meddelelse",
|
|
582
|
+
"Mensagem encaminhada"
|
|
583
|
+
].join("|");
|
|
584
|
+
const dividerPatterns = [
|
|
371
585
|
/_{10,}/m,
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
/
|
|
375
|
-
|
|
586
|
+
/-----\s*(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht|Original meddelande)\s*-----/im,
|
|
587
|
+
/^(Original Message|Alkuperäinen viesti|Ursprüngliches Nachricht|Message d'origine|Mensaje original|Messaggio originale|Oorspronkelijk bericht)$/im,
|
|
588
|
+
// Gmail/Outlook-style "---------- Forwarded message ---------" with localized variants
|
|
589
|
+
new RegExp(`-+\\s*(${forwardedTokens})\\s*-+`, "i"),
|
|
590
|
+
new RegExp(`^(${forwardedTokens})$`, "im")
|
|
376
591
|
];
|
|
592
|
+
const allPatterns = [...wrotePatterns, ...dividerPatterns];
|
|
377
593
|
let earliestCut = text.length;
|
|
378
|
-
for (const pattern of
|
|
594
|
+
for (const pattern of allPatterns) {
|
|
379
595
|
const match = pattern.exec(text);
|
|
380
596
|
if (match && match.index < earliestCut) {
|
|
381
597
|
earliestCut = match.index;
|
|
@@ -384,20 +600,23 @@ function removeNestedQuotes(text) {
|
|
|
384
600
|
return text.substring(0, earliestCut).trim();
|
|
385
601
|
}
|
|
386
602
|
function getUniqueFilepath(saveDir, filename) {
|
|
387
|
-
|
|
388
|
-
let counter = 1;
|
|
389
|
-
const parts = filename.split(".");
|
|
390
|
-
const ext = parts.length > 1 ? `.${parts.pop()}` : "";
|
|
391
|
-
const stem = parts.join(".");
|
|
392
|
-
while (existsSync2(filepath)) {
|
|
393
|
-
filepath = join2(saveDir, `${stem}_${counter}${ext}`);
|
|
394
|
-
counter++;
|
|
395
|
-
}
|
|
396
|
-
return filepath;
|
|
603
|
+
return join2(saveDir, filename);
|
|
397
604
|
}
|
|
398
605
|
async function search_and_fetch_emails(args) {
|
|
399
606
|
const apiKey = await getCloudAuthToken();
|
|
400
|
-
const
|
|
607
|
+
const maxAttachmentSizeMb = typeof args.max_attachment_size_mb === "number" && args.max_attachment_size_mb > 0 ? args.max_attachment_size_mb : 10;
|
|
608
|
+
let realEmailId;
|
|
609
|
+
try {
|
|
610
|
+
realEmailId = args.email_id ? resolveEmailId(args.email_id) : void 0;
|
|
611
|
+
} catch (err) {
|
|
612
|
+
if (err instanceof StaleShortIdError) {
|
|
613
|
+
return {
|
|
614
|
+
isError: true,
|
|
615
|
+
content: [{ type: "text", text: err.message }]
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
throw err;
|
|
619
|
+
}
|
|
401
620
|
const payload = {
|
|
402
621
|
email_id: realEmailId,
|
|
403
622
|
sender: args.sender,
|
|
@@ -408,26 +627,39 @@ async function search_and_fetch_emails(args) {
|
|
|
408
627
|
days_ago: args.days_ago,
|
|
409
628
|
folder: args.folder,
|
|
410
629
|
limit: args.limit ?? 10,
|
|
411
|
-
offset: args.offset ?? 0
|
|
630
|
+
offset: args.offset ?? 0,
|
|
631
|
+
mailbox_address: args.mailbox_address
|
|
412
632
|
};
|
|
413
633
|
Object.keys(payload).forEach(
|
|
414
634
|
(k) => payload[k] === void 0 && delete payload[k]
|
|
415
635
|
);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
636
|
+
let res;
|
|
637
|
+
try {
|
|
638
|
+
res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: {
|
|
641
|
+
Authorization: `Bearer ${apiKey}`,
|
|
642
|
+
"Content-Type": "application/json"
|
|
643
|
+
},
|
|
644
|
+
body: JSON.stringify(payload),
|
|
645
|
+
signal: AbortSignal.timeout(45e3)
|
|
646
|
+
});
|
|
647
|
+
} catch (err) {
|
|
648
|
+
if (isTimeoutError(err)) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
"Email search timed out after 45s. The mail provider (Outlook/Gmail) may be slow. Try narrowing the search with more filters (sender, subject, days_ago), or retry shortly."
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
424
655
|
if (res.status === 401) {
|
|
425
656
|
DesktopAuthManager.clearApiKey();
|
|
426
657
|
throw new Error(
|
|
427
658
|
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
|
|
428
659
|
);
|
|
429
660
|
}
|
|
430
|
-
if (!res.ok)
|
|
661
|
+
if (!res.ok)
|
|
662
|
+
throw new Error(formatBackendError(res.status, await res.text()));
|
|
431
663
|
const data = await res.json();
|
|
432
664
|
const cache = loadIdCache();
|
|
433
665
|
if (data.type === "previews") {
|
|
@@ -459,15 +691,23 @@ async function search_and_fetch_emails(args) {
|
|
|
459
691
|
);
|
|
460
692
|
}
|
|
461
693
|
saveIdCache(cache);
|
|
694
|
+
const limit = typeof args.limit === "number" ? args.limit : 10;
|
|
695
|
+
const offset = typeof args.offset === "number" ? args.offset : 0;
|
|
696
|
+
const pageHint = previews.length >= limit ? `
|
|
697
|
+
*(If you need to see more results, call this tool again with offset=${offset + limit})*` : "";
|
|
462
698
|
lines.push(
|
|
463
|
-
"\u26A0\uFE0F **ACTION REQUIRED**: To read the full body of an email and download its attachments, call this tool again and provide the exact `email_id`."
|
|
699
|
+
"\u26A0\uFE0F **ACTION REQUIRED**: To read the full body of an email and download its attachments, call this tool again and provide the exact `email_id`." + pageHint
|
|
464
700
|
);
|
|
465
|
-
return {
|
|
701
|
+
return {
|
|
702
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
703
|
+
structuredContent: data
|
|
704
|
+
};
|
|
466
705
|
}
|
|
467
706
|
if (data.type === "full_email") {
|
|
468
707
|
const full = data.full_email || {};
|
|
469
708
|
const shortTargetId = minifyEmailId(full.id || "unknown_id", cache);
|
|
470
709
|
saveIdCache(cache);
|
|
710
|
+
const autoEscalated = !args.email_id && (args.sender !== void 0 || args.subject !== void 0 || args.has_attachments !== void 0 || args.attachment_name !== void 0 || args.is_unread !== void 0 || args.days_ago !== void 0 || args.folder !== void 0);
|
|
471
711
|
const baseDir = args.working_directory && existsSync2(args.working_directory) ? args.working_directory : tmpdir();
|
|
472
712
|
const saveDir = join2(
|
|
473
713
|
baseDir,
|
|
@@ -477,35 +717,67 @@ async function search_and_fetch_emails(args) {
|
|
|
477
717
|
mkdirSync2(saveDir, { recursive: true });
|
|
478
718
|
async function processAttachments(msg) {
|
|
479
719
|
const localFiles = [];
|
|
720
|
+
const skipped = [];
|
|
721
|
+
const maxBytes = maxAttachmentSizeMb * 1024 * 1024;
|
|
480
722
|
for (const att of msg.attachments || []) {
|
|
723
|
+
const filename = att.filename || "unnamed_file";
|
|
724
|
+
const size = typeof att.size_bytes === "number" ? att.size_bytes : null;
|
|
725
|
+
if (size != null && size > maxBytes) {
|
|
726
|
+
skipped.push({
|
|
727
|
+
filename,
|
|
728
|
+
size_bytes: size,
|
|
729
|
+
reason: `exceeds ${maxAttachmentSizeMb} MB cap`
|
|
730
|
+
});
|
|
731
|
+
delete att.base64_data;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
481
734
|
if (att.base64_data) {
|
|
482
735
|
try {
|
|
483
|
-
const filepath = getUniqueFilepath(
|
|
484
|
-
saveDir,
|
|
485
|
-
att.filename || "unnamed_file"
|
|
486
|
-
);
|
|
736
|
+
const filepath = getUniqueFilepath(saveDir, filename);
|
|
487
737
|
writeFileSync2(filepath, Buffer.from(att.base64_data, "base64"));
|
|
488
738
|
localFiles.push(filepath);
|
|
739
|
+
att.local_path = filepath;
|
|
489
740
|
delete att.base64_data;
|
|
490
741
|
} catch (e) {
|
|
491
|
-
console.error(`Failed to save attachment ${
|
|
742
|
+
console.error(`Failed to save attachment ${filename}`, e);
|
|
743
|
+
skipped.push({
|
|
744
|
+
filename,
|
|
745
|
+
size_bytes: size,
|
|
746
|
+
reason: `download failed: ${e.message}`
|
|
747
|
+
});
|
|
492
748
|
}
|
|
493
749
|
}
|
|
494
750
|
}
|
|
495
|
-
return localFiles;
|
|
751
|
+
return { localFiles, skipped };
|
|
496
752
|
}
|
|
497
|
-
const targetFiles = await processAttachments(full);
|
|
498
|
-
const lines = [
|
|
753
|
+
const { localFiles: targetFiles, skipped: targetSkipped } = await processAttachments(full);
|
|
754
|
+
const lines = [];
|
|
755
|
+
if (autoEscalated) {
|
|
756
|
+
lines.push(
|
|
757
|
+
"_(Search returned exactly one result; auto-fetched full email below.)_\n"
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
lines.push(
|
|
499
761
|
`# Email Thread: ${full.subject}`,
|
|
500
762
|
"",
|
|
501
763
|
"## Target Message (Newest):",
|
|
502
764
|
`**From**: ${full.sender_name} <${full.sender_email}>`,
|
|
503
765
|
`**Date**: ${full.received_datetime}`
|
|
504
|
-
|
|
766
|
+
);
|
|
505
767
|
if (targetFiles.length) {
|
|
506
768
|
lines.push("**Attachments Saved Locally**:");
|
|
507
769
|
targetFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
|
|
508
770
|
}
|
|
771
|
+
if (targetSkipped.length) {
|
|
772
|
+
lines.push(
|
|
773
|
+
`**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` to raise the ${maxAttachmentSizeMb} MB cap:`
|
|
774
|
+
);
|
|
775
|
+
targetSkipped.forEach(
|
|
776
|
+
(s) => lines.push(
|
|
777
|
+
`- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
|
|
778
|
+
)
|
|
779
|
+
);
|
|
780
|
+
}
|
|
509
781
|
const cleanBody = removeNestedQuotes(stripTags(full.body_html || ""));
|
|
510
782
|
lines.push(`**Body**:
|
|
511
783
|
\`\`\`
|
|
@@ -516,7 +788,7 @@ ${cleanBody}
|
|
|
516
788
|
lines.push("## Previous Messages in Thread (Historical Context):");
|
|
517
789
|
for (let i = 0; i < full.messages.length; i++) {
|
|
518
790
|
const histMsg = full.messages[i];
|
|
519
|
-
const histFiles = await processAttachments(histMsg);
|
|
791
|
+
const { localFiles: histFiles, skipped: histSkipped } = await processAttachments(histMsg);
|
|
520
792
|
lines.push(
|
|
521
793
|
`### Message -${i + 1} (Older)
|
|
522
794
|
**From**: ${histMsg.sender_name} <${histMsg.sender_email}>
|
|
@@ -526,6 +798,16 @@ ${cleanBody}
|
|
|
526
798
|
lines.push("**Attachments Saved Locally**:");
|
|
527
799
|
histFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
|
|
528
800
|
}
|
|
801
|
+
if (histSkipped.length) {
|
|
802
|
+
lines.push(
|
|
803
|
+
`**Attachments Skipped (not downloaded)** \u2014 pass \`max_attachment_size_mb\` \u2014 raise the cap:`
|
|
804
|
+
);
|
|
805
|
+
histSkipped.forEach(
|
|
806
|
+
(s) => lines.push(
|
|
807
|
+
`- \u26A0\uFE0F \`${s.filename}\` (${formatBytes(s.size_bytes)}, ${s.reason})`
|
|
808
|
+
)
|
|
809
|
+
);
|
|
810
|
+
}
|
|
529
811
|
lines.push(
|
|
530
812
|
`**Body**:
|
|
531
813
|
\`\`\`
|
|
@@ -535,7 +817,18 @@ ${removeNestedQuotes(stripTags(histMsg.body_html || ""))}
|
|
|
535
817
|
);
|
|
536
818
|
}
|
|
537
819
|
}
|
|
538
|
-
|
|
820
|
+
const hasAttachments = targetFiles.length > 0 || full.messages && full.messages.some(
|
|
821
|
+
(m) => m.attachments && m.attachments.length > 0
|
|
822
|
+
);
|
|
823
|
+
if (hasAttachments) {
|
|
824
|
+
lines.push(
|
|
825
|
+
"\n*You can now use tools like `read_docx`, `diff_docx_files`, or `finalize_document` on the local file paths listed under each message.*"
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
830
|
+
structuredContent: data
|
|
831
|
+
};
|
|
539
832
|
}
|
|
540
833
|
return {
|
|
541
834
|
isError: true,
|
|
@@ -552,12 +845,25 @@ async function create_email_draft(args) {
|
|
|
552
845
|
const formData = new FormData();
|
|
553
846
|
formData.append("body_markdown", args.body_markdown);
|
|
554
847
|
if (args.reply_to_email_id) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
848
|
+
try {
|
|
849
|
+
formData.append(
|
|
850
|
+
"reply_to_email_id",
|
|
851
|
+
resolveEmailId(args.reply_to_email_id)
|
|
852
|
+
);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (err instanceof StaleShortIdError) {
|
|
855
|
+
return {
|
|
856
|
+
isError: true,
|
|
857
|
+
content: [{ type: "text", text: err.message }]
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
throw err;
|
|
861
|
+
}
|
|
559
862
|
}
|
|
560
863
|
if (args.subject) formData.append("subject", args.subject);
|
|
864
|
+
if (args.mailbox_address) {
|
|
865
|
+
formData.append("mailbox_address", args.mailbox_address);
|
|
866
|
+
}
|
|
561
867
|
if (args.to_recipients) {
|
|
562
868
|
const recips = typeof args.to_recipients === "string" ? JSON.parse(args.to_recipients) : args.to_recipients;
|
|
563
869
|
formData.append("to_recipients", JSON.stringify(recips));
|
|
@@ -570,11 +876,25 @@ async function create_email_draft(args) {
|
|
|
570
876
|
formData.append("files", new Blob([buf]), filename);
|
|
571
877
|
}
|
|
572
878
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
879
|
+
let res;
|
|
880
|
+
try {
|
|
881
|
+
res = await fetch(`${BACKEND_URL}/api/v1/emails/drafts/new`, {
|
|
882
|
+
method: "POST",
|
|
883
|
+
headers: {
|
|
884
|
+
Authorization: `Bearer ${apiKey}`,
|
|
885
|
+
Accept: "application/json"
|
|
886
|
+
},
|
|
887
|
+
body: formData,
|
|
888
|
+
signal: AbortSignal.timeout(9e4)
|
|
889
|
+
});
|
|
890
|
+
} catch (err) {
|
|
891
|
+
if (isTimeoutError(err)) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
"Draft creation timed out after 90s. If the draft includes large attachments, try splitting them across multiple drafts or omitting the largest files."
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
throw err;
|
|
897
|
+
}
|
|
578
898
|
if (res.status === 401) {
|
|
579
899
|
DesktopAuthManager.clearApiKey();
|
|
580
900
|
throw new Error(
|
|
@@ -582,7 +902,7 @@ async function create_email_draft(args) {
|
|
|
582
902
|
);
|
|
583
903
|
}
|
|
584
904
|
if (!res.ok)
|
|
585
|
-
throw new Error(
|
|
905
|
+
throw new Error(formatBackendError(res.status, await res.text()));
|
|
586
906
|
const data = await res.json();
|
|
587
907
|
return {
|
|
588
908
|
content: [
|
|
@@ -593,6 +913,66 @@ async function create_email_draft(args) {
|
|
|
593
913
|
]
|
|
594
914
|
};
|
|
595
915
|
}
|
|
916
|
+
async function list_available_mailboxes() {
|
|
917
|
+
const apiKey = await getCloudAuthToken();
|
|
918
|
+
let res;
|
|
919
|
+
try {
|
|
920
|
+
res = await fetch(`${BACKEND_URL}/api/v1/users/me/shared-mailboxes`, {
|
|
921
|
+
method: "GET",
|
|
922
|
+
headers: {
|
|
923
|
+
Authorization: `Bearer ${apiKey}`,
|
|
924
|
+
Accept: "application/json"
|
|
925
|
+
},
|
|
926
|
+
signal: AbortSignal.timeout(15e3)
|
|
927
|
+
});
|
|
928
|
+
} catch (err) {
|
|
929
|
+
if (isTimeoutError(err)) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
"Listing mailboxes timed out after 15s. The Adeu backend may be temporarily unavailable; retry shortly."
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
throw err;
|
|
935
|
+
}
|
|
936
|
+
if (res.status === 401) {
|
|
937
|
+
DesktopAuthManager.clearApiKey();
|
|
938
|
+
throw new Error(
|
|
939
|
+
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (!res.ok) {
|
|
943
|
+
throw new Error(formatBackendError(res.status, await res.text()));
|
|
944
|
+
}
|
|
945
|
+
const mailboxes = await res.json();
|
|
946
|
+
if (!mailboxes.length) {
|
|
947
|
+
return {
|
|
948
|
+
content: [
|
|
949
|
+
{
|
|
950
|
+
type: "text",
|
|
951
|
+
text: "No configured mailboxes found for your profile."
|
|
952
|
+
}
|
|
953
|
+
]
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
mailboxes.sort(
|
|
957
|
+
(a, b) => (a.email_address ?? "").toLowerCase().localeCompare((b.email_address ?? "").toLowerCase())
|
|
958
|
+
);
|
|
959
|
+
const lines = [
|
|
960
|
+
"### Connected Mailboxes",
|
|
961
|
+
"Below is the list of connected mailboxes you have access to. Use the `email_address` as the `mailbox_address` parameter in other tools to query or draft from a specific mailbox:",
|
|
962
|
+
""
|
|
963
|
+
];
|
|
964
|
+
for (const box of mailboxes) {
|
|
965
|
+
lines.push(
|
|
966
|
+
`- **${box.display_name || "Personal Mailbox"}**
|
|
967
|
+
- **Email Address**: \`${box.email_address}\`
|
|
968
|
+
- **Auto-Processing**: ${box.auto_process_enabled ? "Enabled" : "Disabled"}
|
|
969
|
+
- **Write-Back Mode**: \`${box.write_back_preference}\``
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
974
|
+
};
|
|
975
|
+
}
|
|
596
976
|
|
|
597
977
|
// src/index.ts
|
|
598
978
|
function readFileBytesOrThrow(filePath) {
|
|
@@ -605,416 +985,490 @@ function readFileBytesOrThrow(filePath) {
|
|
|
605
985
|
throw err;
|
|
606
986
|
}
|
|
607
987
|
}
|
|
988
|
+
var DIST_DIR = import.meta.dirname;
|
|
989
|
+
function getAssetContent(folder, filename, fallbackMessage) {
|
|
990
|
+
const filePath = join3(DIST_DIR, folder, filename);
|
|
991
|
+
if (existsSync3(filePath)) {
|
|
992
|
+
return readFileSync3(filePath, "utf-8");
|
|
993
|
+
}
|
|
994
|
+
return fallbackMessage;
|
|
995
|
+
}
|
|
608
996
|
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";
|
|
609
997
|
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.";
|
|
610
998
|
var PROCESS_BATCH_COMMON_DESC = "Applies a batch of edits and review actions to a DOCX.\n\nAll changes evaluate against the ORIGINAL document state \u2014 do not chain dependent edits within one batch (e.g. rename X to Y, then modify Y). Apply the rename first, then send a second batch.\n\n";
|
|
611
999
|
var PROCESS_BATCH_OPERATIONS_DESC = "Each item in `changes` must specify a `type`:\n1. 'modify': Search-and-replace. `target_text` must uniquely match \u2014 include surrounding context if the phrase is ambiguous. `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.\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.\n\n`author_name` is used for attribution on all tracked changes and comments, in both disk and Live Word modes.";
|
|
612
1000
|
var DIFF_DOCX_DESC = "Compares two DOCX files and returns a unified diff of their text content. Useful for analyzing differences between versions before editing.";
|
|
613
|
-
var server = new
|
|
1001
|
+
var server = new McpServer({
|
|
1002
|
+
name: "adeu-redlining-service",
|
|
1003
|
+
version: "1.0.0"
|
|
1004
|
+
});
|
|
1005
|
+
var UI_CSP = {
|
|
1006
|
+
connectDomains: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
|
|
1007
|
+
resourceDomains: [
|
|
1008
|
+
"https://fonts.googleapis.com",
|
|
1009
|
+
"https://fonts.gstatic.com"
|
|
1010
|
+
]
|
|
1011
|
+
};
|
|
1012
|
+
registerAppResource(
|
|
1013
|
+
server,
|
|
1014
|
+
MARKDOWN_UI_URI,
|
|
1015
|
+
MARKDOWN_UI_URI,
|
|
1016
|
+
{ mimeType: RESOURCE_MIME_TYPE, description: "Adeu Markdown Viewer UI" },
|
|
1017
|
+
async () => {
|
|
1018
|
+
let html = getAssetContent(
|
|
1019
|
+
"templates",
|
|
1020
|
+
"markdown_ui.html",
|
|
1021
|
+
"<html><body>UI Template Not Found</body></html>"
|
|
1022
|
+
);
|
|
1023
|
+
const markedJs = getAssetContent(
|
|
1024
|
+
"assets",
|
|
1025
|
+
"marked.min.js",
|
|
1026
|
+
"window.__MARKED_ERROR = 'marked.min.js not found';"
|
|
1027
|
+
);
|
|
1028
|
+
const svg = getAssetContent("assets", "adeu.svg", "");
|
|
1029
|
+
html = html.replace("[[marked_js_code | safe]]", markedJs).replace("[[ adeu_svg_code ]]", svg);
|
|
1030
|
+
return {
|
|
1031
|
+
contents: [
|
|
1032
|
+
{
|
|
1033
|
+
uri: MARKDOWN_UI_URI,
|
|
1034
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
1035
|
+
text: html,
|
|
1036
|
+
_meta: { ui: { csp: UI_CSP } }
|
|
1037
|
+
}
|
|
1038
|
+
]
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
registerAppResource(
|
|
1043
|
+
server,
|
|
1044
|
+
EMAIL_UI_URI,
|
|
1045
|
+
EMAIL_UI_URI,
|
|
1046
|
+
{ mimeType: RESOURCE_MIME_TYPE, description: "Adeu Email Viewer UI" },
|
|
1047
|
+
async () => {
|
|
1048
|
+
let html = getAssetContent(
|
|
1049
|
+
"templates",
|
|
1050
|
+
"email_ui.html",
|
|
1051
|
+
"<html><body>UI Template Not Found</body></html>"
|
|
1052
|
+
);
|
|
1053
|
+
const svg = getAssetContent("assets", "adeu.svg", "");
|
|
1054
|
+
html = html.replace("[[ adeu_svg_code ]]", svg);
|
|
1055
|
+
return {
|
|
1056
|
+
contents: [
|
|
1057
|
+
{
|
|
1058
|
+
uri: EMAIL_UI_URI,
|
|
1059
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
1060
|
+
text: html,
|
|
1061
|
+
_meta: { ui: { csp: UI_CSP } }
|
|
1062
|
+
}
|
|
1063
|
+
]
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
registerAppTool(
|
|
1068
|
+
server,
|
|
1069
|
+
"read_docx",
|
|
614
1070
|
{
|
|
615
|
-
|
|
616
|
-
|
|
1071
|
+
title: "Read DOCX",
|
|
1072
|
+
description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,
|
|
1073
|
+
inputSchema: z.object({
|
|
1074
|
+
file_path: z.string().describe("Absolute path to the DOCX file."),
|
|
1075
|
+
clean_view: z.boolean().default(false).describe(
|
|
1076
|
+
"If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text."
|
|
1077
|
+
),
|
|
1078
|
+
mode: z.enum(["full", "outline", "appendix"]).default("full").describe(
|
|
1079
|
+
"'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms."
|
|
1080
|
+
),
|
|
1081
|
+
page: z.number().default(1).describe("Page number (1-indexed) for mode='full'. Defaults to 1."),
|
|
1082
|
+
outline_max_level: z.number().default(2).describe("For mode='outline' only: cap on heading depth."),
|
|
1083
|
+
outline_verbose: z.boolean().default(false).describe("For mode='outline' only: includes metadata.")
|
|
1084
|
+
}),
|
|
1085
|
+
_meta: { ui: { resourceUri: MARKDOWN_UI_URI } }
|
|
617
1086
|
},
|
|
1087
|
+
async ({
|
|
1088
|
+
file_path,
|
|
1089
|
+
clean_view,
|
|
1090
|
+
mode,
|
|
1091
|
+
page,
|
|
1092
|
+
outline_max_level,
|
|
1093
|
+
outline_verbose
|
|
1094
|
+
}) => {
|
|
1095
|
+
try {
|
|
1096
|
+
const buf = readFileBytesOrThrow(file_path);
|
|
1097
|
+
const text = await extractTextFromBuffer(buf, clean_view);
|
|
1098
|
+
if (mode === "outline") {
|
|
1099
|
+
const doc = await DocumentObject2.load(buf);
|
|
1100
|
+
return build_outline_response(
|
|
1101
|
+
doc,
|
|
1102
|
+
text,
|
|
1103
|
+
file_path,
|
|
1104
|
+
outline_max_level,
|
|
1105
|
+
outline_verbose
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
if (mode === "appendix") {
|
|
1109
|
+
return build_appendix_response(text, page, file_path);
|
|
1110
|
+
}
|
|
1111
|
+
return build_paginated_response(text, page, file_path);
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
return {
|
|
1114
|
+
isError: true,
|
|
1115
|
+
content: [
|
|
1116
|
+
{
|
|
1117
|
+
type: "text",
|
|
1118
|
+
text: `Error executing tool read_docx: ${e.message}`
|
|
1119
|
+
}
|
|
1120
|
+
]
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
registerAppTool(
|
|
1126
|
+
server,
|
|
1127
|
+
"search_and_fetch_emails",
|
|
618
1128
|
{
|
|
619
|
-
|
|
620
|
-
|
|
1129
|
+
title: "Search & Fetch Emails",
|
|
1130
|
+
description: "Searches the user's live email inbox via the Adeu cloud backend.\n\nTWO MODES:\n1. Search mode (no `email_id`): returns up to `limit` lightweight previews. Use filters (`sender`, `subject`, `is_unread`, `days_ago`, `folder`, `has_attachments`, `attachment_name`) to narrow down.\n2. Fetch mode (with `email_id`): returns the full email body, thread history, and downloads attachments under `max_attachment_size_mb` to the local disk.\n\nAUTO-ESCALATION: If a search returns exactly one preview, the backend automatically fetches the full email in the same call. Plan around the response shape \u2014 check the `type` field (`previews` vs `full_email`) before assuming.\n\nEMAIL ID FORMATS (`email_id` parameter accepts any of):\n- `msg_<6 chars>` \u2014 short ID returned by previews on THIS machine. NOT portable across machines or sessions; the local cache holds the most recent 1000. If you reference one that's been evicted, the tool returns a StaleShortIdError telling you to re-search.\n- `adeu_<numeric>` \u2014 server-side reference for emails Adeu has previously processed. Portable across machines and sessions for the same authenticated user.\n- Raw provider ID (Gmail/Outlook native ID) \u2014 works if you have it, but you usually won't.\n\nFOLDER DEFAULT: omitting `folder` searches the Inbox only (matching what the user sees in their mail client). Use `folder='sent'` for sent items, `folder='all'` to include Deleted Items, Drafts, and other folders.\n\nATTACHMENTS: attachments larger than `max_attachment_size_mb` (default 10) are listed in the response but NOT downloaded \u2014 raise the cap if you need them. Always set `working_directory` when calling from a project so attachments land alongside the user's other files.",
|
|
1131
|
+
inputSchema: z.object({
|
|
1132
|
+
sender: z.string().optional(),
|
|
1133
|
+
subject: z.string().optional(),
|
|
1134
|
+
has_attachments: z.boolean().optional(),
|
|
1135
|
+
attachment_name: z.string().optional(),
|
|
1136
|
+
is_unread: z.boolean().optional(),
|
|
1137
|
+
days_ago: z.number().optional(),
|
|
1138
|
+
folder: z.enum(["inbox", "sent", "all"]).optional(),
|
|
1139
|
+
limit: z.number().default(10),
|
|
1140
|
+
offset: z.number().default(0),
|
|
1141
|
+
email_id: z.string().optional(),
|
|
1142
|
+
working_directory: z.string().optional(),
|
|
1143
|
+
mailbox_address: z.string().optional().describe("Optional target mailbox email address to search within."),
|
|
1144
|
+
max_attachment_size_mb: z.number().optional().describe(
|
|
1145
|
+
"Maximum attachment size in MB to download (default 10). Attachments larger than this are listed in the response but not downloaded. Raise this to fetch large files."
|
|
1146
|
+
)
|
|
1147
|
+
}),
|
|
1148
|
+
_meta: { ui: { resourceUri: EMAIL_UI_URI } }
|
|
1149
|
+
},
|
|
1150
|
+
async (args) => {
|
|
1151
|
+
try {
|
|
1152
|
+
return await search_and_fetch_emails(args);
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
return {
|
|
1155
|
+
isError: true,
|
|
1156
|
+
content: [{ type: "text", text: e.message }]
|
|
1157
|
+
};
|
|
621
1158
|
}
|
|
622
1159
|
}
|
|
623
1160
|
);
|
|
624
|
-
server.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
clean_view: {
|
|
638
|
-
type: "boolean",
|
|
639
|
-
description: "If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text.",
|
|
640
|
-
default: false
|
|
641
|
-
},
|
|
642
|
-
mode: {
|
|
643
|
-
type: "string",
|
|
644
|
-
enum: ["full", "outline", "appendix"],
|
|
645
|
-
description: "'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms.",
|
|
646
|
-
default: "full"
|
|
647
|
-
},
|
|
648
|
-
page: {
|
|
649
|
-
type: "number",
|
|
650
|
-
description: "Page number (1-indexed) for mode='full'. Defaults to 1.",
|
|
651
|
-
default: 1
|
|
652
|
-
},
|
|
653
|
-
outline_max_level: {
|
|
654
|
-
type: "number",
|
|
655
|
-
description: "For mode='outline' only: cap on heading depth.",
|
|
656
|
-
default: 2
|
|
657
|
-
},
|
|
658
|
-
outline_verbose: {
|
|
659
|
-
type: "boolean",
|
|
660
|
-
description: "For mode='outline' only: includes metadata.",
|
|
661
|
-
default: false
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
required: ["file_path"]
|
|
665
|
-
}
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
name: "process_document_batch",
|
|
669
|
-
description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,
|
|
670
|
-
inputSchema: {
|
|
671
|
-
type: "object",
|
|
672
|
-
properties: {
|
|
673
|
-
original_docx_path: {
|
|
674
|
-
type: "string",
|
|
675
|
-
description: "Absolute path to the source file."
|
|
676
|
-
},
|
|
677
|
-
author_name: {
|
|
678
|
-
type: "string",
|
|
679
|
-
description: "Name to appear in Track Changes (e.g., 'Reviewer AI')."
|
|
680
|
-
},
|
|
681
|
-
changes: {
|
|
682
|
-
type: "array",
|
|
683
|
-
description: "List of changes to apply. Each change must specify 'type'.",
|
|
684
|
-
items: { type: "object" }
|
|
685
|
-
},
|
|
686
|
-
output_path: {
|
|
687
|
-
type: "string",
|
|
688
|
-
description: "Optional output path."
|
|
689
|
-
}
|
|
690
|
-
},
|
|
691
|
-
required: ["original_docx_path", "author_name", "changes"]
|
|
692
|
-
}
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
name: "accept_all_changes",
|
|
696
|
-
description: "Accepts all tracked changes and removes all comments in a single operation, producing a finalized clean document. Use this when a document review is entirely complete and you want to clear all redlines.",
|
|
697
|
-
inputSchema: {
|
|
698
|
-
type: "object",
|
|
699
|
-
properties: {
|
|
700
|
-
docx_path: {
|
|
701
|
-
type: "string",
|
|
702
|
-
description: "Absolute path to the DOCX file."
|
|
703
|
-
},
|
|
704
|
-
output_path: {
|
|
705
|
-
type: "string",
|
|
706
|
-
description: "Optional output path."
|
|
707
|
-
}
|
|
708
|
-
},
|
|
709
|
-
required: ["docx_path"]
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
|
-
{
|
|
713
|
-
name: "diff_docx_files",
|
|
714
|
-
description: DIFF_DOCX_DESC,
|
|
715
|
-
inputSchema: {
|
|
716
|
-
type: "object",
|
|
717
|
-
properties: {
|
|
718
|
-
original_path: {
|
|
719
|
-
type: "string",
|
|
720
|
-
description: "Absolute path to the baseline DOCX file."
|
|
721
|
-
},
|
|
722
|
-
modified_path: {
|
|
723
|
-
type: "string",
|
|
724
|
-
description: "Absolute path to the modified DOCX file."
|
|
725
|
-
},
|
|
726
|
-
compare_clean: {
|
|
727
|
-
type: "boolean",
|
|
728
|
-
description: "If True, compares 'Accepted' state. If False, compares raw text.",
|
|
729
|
-
default: true
|
|
730
|
-
}
|
|
731
|
-
},
|
|
732
|
-
required: ["original_path", "modified_path"]
|
|
733
|
-
}
|
|
734
|
-
},
|
|
735
|
-
{
|
|
736
|
-
name: "finalize_document",
|
|
737
|
-
description: "Prepares a document for external distribution or e-signature. This tool combines metadata sanitization, document locking (protection), and markup resolution into a single step. NOTE: PDF export and AES encryption are disabled in this environment.",
|
|
738
|
-
inputSchema: {
|
|
739
|
-
type: "object",
|
|
740
|
-
properties: {
|
|
741
|
-
file_path: {
|
|
742
|
-
type: "string",
|
|
743
|
-
description: "Absolute path to the DOCX file."
|
|
744
|
-
},
|
|
745
|
-
output_path: {
|
|
746
|
-
type: "string",
|
|
747
|
-
description: "Optional output path."
|
|
748
|
-
},
|
|
749
|
-
sanitize_mode: {
|
|
750
|
-
type: "string",
|
|
751
|
-
enum: ["full", "keep-markup"],
|
|
752
|
-
description: "full removes all markup, keep-markup redacts metadata but keeps comments/redlines."
|
|
753
|
-
},
|
|
754
|
-
accept_all: {
|
|
755
|
-
type: "boolean",
|
|
756
|
-
description: "If true, auto-accepts all unresolved track changes before finalizing."
|
|
757
|
-
},
|
|
758
|
-
protection_mode: {
|
|
759
|
-
type: "string",
|
|
760
|
-
enum: ["read_only", "encrypt"],
|
|
761
|
-
description: "Native OOXML document locking. encrypt falls back to read_only in this environment."
|
|
762
|
-
},
|
|
763
|
-
password: {
|
|
764
|
-
type: "string",
|
|
765
|
-
description: "Ignored in this environment."
|
|
766
|
-
},
|
|
767
|
-
author: {
|
|
768
|
-
type: "string",
|
|
769
|
-
description: "Replace all remaining markup authorship with this name."
|
|
770
|
-
},
|
|
771
|
-
export_pdf: {
|
|
772
|
-
type: "boolean",
|
|
773
|
-
description: "Ignored in this environment."
|
|
774
|
-
}
|
|
775
|
-
},
|
|
776
|
-
required: ["file_path"]
|
|
777
|
-
}
|
|
778
|
-
},
|
|
779
|
-
{
|
|
780
|
-
name: "login_to_adeu_cloud",
|
|
781
|
-
description: "Logs the user into the Adeu Cloud backend. Securely opens a browser window for authentication.",
|
|
782
|
-
inputSchema: { type: "object", properties: {} }
|
|
783
|
-
},
|
|
784
|
-
{
|
|
785
|
-
name: "logout_of_adeu_cloud",
|
|
786
|
-
description: "Logs out of the Adeu Cloud backend by clearing the local API key.",
|
|
787
|
-
inputSchema: { type: "object", properties: {} }
|
|
788
|
-
},
|
|
789
|
-
{
|
|
790
|
-
name: "search_and_fetch_emails",
|
|
791
|
-
description: "Searches the user's live email inbox. By default, searches only the Inbox folder. Returns a list of lightweight previews. Call again with `email_id` to fetch the full body and download attachments.",
|
|
792
|
-
inputSchema: {
|
|
793
|
-
type: "object",
|
|
794
|
-
properties: {
|
|
795
|
-
sender: { type: "string" },
|
|
796
|
-
subject: { type: "string" },
|
|
797
|
-
has_attachments: { type: "boolean" },
|
|
798
|
-
attachment_name: { type: "string" },
|
|
799
|
-
is_unread: { type: "boolean" },
|
|
800
|
-
days_ago: { type: "number" },
|
|
801
|
-
folder: { type: "string", enum: ["inbox", "sent", "all"] },
|
|
802
|
-
limit: { type: "number", default: 10 },
|
|
803
|
-
offset: { type: "number", default: 0 },
|
|
804
|
-
email_id: { type: "string" },
|
|
805
|
-
working_directory: { type: "string" }
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
name: "create_email_draft",
|
|
811
|
-
description: "Creates an email draft in the user's native draft box. Provide `reply_to_email_id` to reply, or `subject` and `to_recipients` for a new email.",
|
|
812
|
-
inputSchema: {
|
|
813
|
-
type: "object",
|
|
814
|
-
properties: {
|
|
815
|
-
body_markdown: { type: "string" },
|
|
816
|
-
reply_to_email_id: { type: "string" },
|
|
817
|
-
subject: { type: "string" },
|
|
818
|
-
to_recipients: { type: "array", items: { type: "string" } },
|
|
819
|
-
attachment_paths: { type: "array", items: { type: "string" } }
|
|
820
|
-
},
|
|
821
|
-
required: ["body_markdown"]
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
]
|
|
825
|
-
};
|
|
826
|
-
});
|
|
827
|
-
server.setRequestHandler(
|
|
828
|
-
CallToolRequestSchema,
|
|
829
|
-
async (request) => {
|
|
830
|
-
const { name, arguments: args } = request.params;
|
|
1161
|
+
server.registerTool(
|
|
1162
|
+
"process_document_batch",
|
|
1163
|
+
{
|
|
1164
|
+
description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,
|
|
1165
|
+
inputSchema: {
|
|
1166
|
+
original_docx_path: z.string().describe("Absolute path to the source file."),
|
|
1167
|
+
author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
|
|
1168
|
+
changes: z.array(z.any()).describe("List of changes to apply. Each change must specify 'type'."),
|
|
1169
|
+
output_path: z.string().optional().describe("Optional output path."),
|
|
1170
|
+
dry_run: z.boolean().optional().default(false).describe("If True, simulates the changes and returns a detailed preview report without modifying any files.")
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
async ({ original_docx_path, author_name, changes, output_path, dry_run }) => {
|
|
831
1174
|
try {
|
|
832
|
-
if (
|
|
833
|
-
const filePath = args?.file_path;
|
|
834
|
-
const cleanView = args?.clean_view ?? false;
|
|
835
|
-
const mode = args?.mode ?? "full";
|
|
836
|
-
const page = args?.page ?? 1;
|
|
837
|
-
const outline_max_level = args?.outline_max_level ?? 2;
|
|
838
|
-
const outline_verbose = args?.outline_verbose ?? false;
|
|
839
|
-
const buf = readFileBytesOrThrow(filePath);
|
|
840
|
-
const text = await extractTextFromBuffer(buf, cleanView);
|
|
841
|
-
if (mode === "outline") {
|
|
842
|
-
const doc = await DocumentObject2.load(buf);
|
|
843
|
-
return build_outline_response(
|
|
844
|
-
doc,
|
|
845
|
-
text,
|
|
846
|
-
filePath,
|
|
847
|
-
outline_max_level,
|
|
848
|
-
outline_verbose
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
if (mode === "appendix") {
|
|
852
|
-
return build_appendix_response(text, page, filePath);
|
|
853
|
-
}
|
|
854
|
-
return build_paginated_response(text, page, filePath);
|
|
855
|
-
}
|
|
856
|
-
if (name === "process_document_batch") {
|
|
857
|
-
const origPath = args?.original_docx_path;
|
|
858
|
-
const authorName = args?.author_name;
|
|
859
|
-
const changes = args?.changes;
|
|
860
|
-
let outPath = args?.output_path;
|
|
861
|
-
if (!authorName || !authorName.trim()) {
|
|
862
|
-
return {
|
|
863
|
-
content: [
|
|
864
|
-
{ type: "text", text: "Error: author_name cannot be empty." }
|
|
865
|
-
]
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
if (!changes || changes.length === 0) {
|
|
869
|
-
return {
|
|
870
|
-
content: [{ type: "text", text: "Error: No changes provided." }]
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
if (!outPath) {
|
|
874
|
-
const ext = extname(origPath);
|
|
875
|
-
const base = basename2(origPath, ext);
|
|
876
|
-
const dir = dirname(origPath);
|
|
877
|
-
outPath = resolve2(dir, `${base}_processed${ext}`);
|
|
878
|
-
}
|
|
879
|
-
const buf = readFileBytesOrThrow(origPath);
|
|
880
|
-
const doc = await DocumentObject2.load(buf);
|
|
881
|
-
const engine = new RedlineEngine(doc, authorName);
|
|
882
|
-
let stats;
|
|
883
|
-
try {
|
|
884
|
-
stats = engine.process_batch(changes);
|
|
885
|
-
} catch (e) {
|
|
886
|
-
if (e instanceof BatchValidationError) {
|
|
887
|
-
return {
|
|
888
|
-
content: [
|
|
889
|
-
{
|
|
890
|
-
type: "text",
|
|
891
|
-
text: `Batch rejected. Some edits failed validation:
|
|
892
|
-
|
|
893
|
-
${e.errors.join("\n\n")}`
|
|
894
|
-
}
|
|
895
|
-
],
|
|
896
|
-
isError: true
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
throw e;
|
|
900
|
-
}
|
|
901
|
-
const outBuf = await doc.save();
|
|
902
|
-
const fs = await import("fs");
|
|
903
|
-
fs.writeFileSync(outPath, outBuf);
|
|
904
|
-
let res = `Batch complete. Saved to: ${outPath}
|
|
905
|
-
Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
|
|
906
|
-
Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.`;
|
|
907
|
-
if (stats.skipped_details?.length > 0) {
|
|
908
|
-
res += `
|
|
909
|
-
|
|
910
|
-
Skipped Details:
|
|
911
|
-
${stats.skipped_details.join("\n")}`;
|
|
912
|
-
}
|
|
913
|
-
return {
|
|
914
|
-
content: [{ type: "text", text: res }]
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
if (name === "accept_all_changes") {
|
|
918
|
-
const docxPath = args?.docx_path;
|
|
919
|
-
let outPath = args?.output_path;
|
|
920
|
-
if (!outPath) {
|
|
921
|
-
const ext = extname(docxPath);
|
|
922
|
-
const base = basename2(docxPath, ext);
|
|
923
|
-
const dir = dirname(docxPath);
|
|
924
|
-
outPath = resolve2(dir, `${base}_clean${ext}`);
|
|
925
|
-
}
|
|
926
|
-
const buf = readFileBytesOrThrow(docxPath);
|
|
927
|
-
const doc = await DocumentObject2.load(buf);
|
|
928
|
-
const engine = new RedlineEngine(doc);
|
|
929
|
-
engine.accept_all_revisions();
|
|
930
|
-
const outBuf = await doc.save();
|
|
931
|
-
const fs = await import("fs");
|
|
932
|
-
fs.writeFileSync(outPath, outBuf);
|
|
1175
|
+
if (!author_name || !author_name.trim())
|
|
933
1176
|
return {
|
|
934
1177
|
content: [
|
|
935
|
-
{
|
|
936
|
-
type: "text",
|
|
937
|
-
text: `Accepted all changes. Saved to: ${outPath}`
|
|
938
|
-
}
|
|
1178
|
+
{ type: "text", text: "Error: author_name cannot be empty." }
|
|
939
1179
|
]
|
|
940
1180
|
};
|
|
941
|
-
|
|
942
|
-
if (name === "diff_docx_files") {
|
|
943
|
-
const origPath = args?.original_path;
|
|
944
|
-
const modPath = args?.modified_path;
|
|
945
|
-
const compareClean = args?.compare_clean ?? true;
|
|
946
|
-
const origBuf = readFileBytesOrThrow(origPath);
|
|
947
|
-
const modBuf = readFileBytesOrThrow(modPath);
|
|
948
|
-
const origText = await extractTextFromBuffer(origBuf, compareClean);
|
|
949
|
-
const modText = await extractTextFromBuffer(modBuf, compareClean);
|
|
950
|
-
const diff = create_word_patch_diff(
|
|
951
|
-
origText,
|
|
952
|
-
modText,
|
|
953
|
-
basename2(origPath),
|
|
954
|
-
basename2(modPath)
|
|
955
|
-
);
|
|
1181
|
+
if (!changes || changes.length === 0)
|
|
956
1182
|
return {
|
|
957
|
-
content: [{ type: "text", text:
|
|
1183
|
+
content: [{ type: "text", text: "Error: No changes provided." }]
|
|
958
1184
|
};
|
|
1185
|
+
let outPath = output_path;
|
|
1186
|
+
if (!outPath) {
|
|
1187
|
+
const ext = extname(original_docx_path);
|
|
1188
|
+
const base = basename2(original_docx_path, ext);
|
|
1189
|
+
const dir = dirname(original_docx_path);
|
|
1190
|
+
outPath = resolve2(dir, `${base}_processed${ext}`);
|
|
959
1191
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
accept_all: args?.accept_all,
|
|
975
|
-
protection_mode: args?.protection_mode,
|
|
976
|
-
author: args?.author,
|
|
977
|
-
export_pdf: args?.export_pdf
|
|
978
|
-
});
|
|
979
|
-
const fs = await import("fs");
|
|
980
|
-
fs.writeFileSync(outPath, result.outBuffer);
|
|
981
|
-
return {
|
|
982
|
-
content: [
|
|
983
|
-
{
|
|
984
|
-
type: "text",
|
|
985
|
-
text: `Saved to: ${outPath}
|
|
1192
|
+
const buf = readFileBytesOrThrow(original_docx_path);
|
|
1193
|
+
const doc = await DocumentObject2.load(buf);
|
|
1194
|
+
const engine = new RedlineEngine(doc, author_name);
|
|
1195
|
+
let stats;
|
|
1196
|
+
try {
|
|
1197
|
+
stats = engine.process_batch(changes, dry_run);
|
|
1198
|
+
} catch (e) {
|
|
1199
|
+
if (e instanceof BatchValidationError) {
|
|
1200
|
+
return {
|
|
1201
|
+
isError: true,
|
|
1202
|
+
content: [
|
|
1203
|
+
{
|
|
1204
|
+
type: "text",
|
|
1205
|
+
text: `Batch rejected. Some edits failed validation:
|
|
986
1206
|
|
|
987
|
-
${
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
return await login_to_adeu_cloud();
|
|
1207
|
+
${e.errors.join("\n\n")}`
|
|
1208
|
+
}
|
|
1209
|
+
]
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
throw e;
|
|
994
1213
|
}
|
|
995
|
-
if (
|
|
996
|
-
|
|
1214
|
+
if (!dry_run) {
|
|
1215
|
+
const outBuf = await doc.save();
|
|
1216
|
+
fs.writeFileSync(outPath, outBuf);
|
|
997
1217
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1218
|
+
const res = formatBatchResult(stats, outPath, !!dry_run);
|
|
1219
|
+
return { content: [{ type: "text", text: res }] };
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
return {
|
|
1222
|
+
isError: true,
|
|
1223
|
+
content: [{ type: "text", text: `Error: ${e.message}` }]
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
server.registerTool(
|
|
1229
|
+
"accept_all_changes",
|
|
1230
|
+
{
|
|
1231
|
+
description: "Accepts all tracked changes and removes all comments in a single operation.",
|
|
1232
|
+
inputSchema: {
|
|
1233
|
+
docx_path: z.string().describe("Absolute path to the DOCX file."),
|
|
1234
|
+
output_path: z.string().optional().describe("Optional output path.")
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
async ({ docx_path, output_path }) => {
|
|
1238
|
+
try {
|
|
1239
|
+
let outPath = output_path;
|
|
1240
|
+
if (!outPath) {
|
|
1241
|
+
const ext = extname(docx_path);
|
|
1242
|
+
const base = basename2(docx_path, ext);
|
|
1243
|
+
const dir = dirname(docx_path);
|
|
1244
|
+
outPath = resolve2(dir, `${base}_clean${ext}`);
|
|
1000
1245
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1246
|
+
const buf = readFileBytesOrThrow(docx_path);
|
|
1247
|
+
const doc = await DocumentObject2.load(buf);
|
|
1248
|
+
const engine = new RedlineEngine(doc);
|
|
1249
|
+
engine.accept_all_revisions();
|
|
1250
|
+
const outBuf = await doc.save();
|
|
1251
|
+
fs.writeFileSync(outPath, outBuf);
|
|
1252
|
+
return {
|
|
1253
|
+
content: [
|
|
1254
|
+
{ type: "text", text: `Accepted all changes. Saved to: ${outPath}` }
|
|
1255
|
+
]
|
|
1256
|
+
};
|
|
1257
|
+
} catch (e) {
|
|
1258
|
+
return {
|
|
1259
|
+
isError: true,
|
|
1260
|
+
content: [{ type: "text", text: `Error: ${e.message}` }]
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
);
|
|
1265
|
+
server.registerTool(
|
|
1266
|
+
"diff_docx_files",
|
|
1267
|
+
{
|
|
1268
|
+
description: DIFF_DOCX_DESC,
|
|
1269
|
+
inputSchema: {
|
|
1270
|
+
original_path: z.string().describe("Absolute path to the baseline DOCX file."),
|
|
1271
|
+
modified_path: z.string().describe("Absolute path to the modified DOCX file."),
|
|
1272
|
+
compare_clean: z.boolean().default(true).describe(
|
|
1273
|
+
"If True, compares 'Accepted' state. If False, compares raw text."
|
|
1274
|
+
)
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
async ({ original_path, modified_path, compare_clean }) => {
|
|
1278
|
+
try {
|
|
1279
|
+
const origBuf = readFileBytesOrThrow(original_path);
|
|
1280
|
+
const modBuf = readFileBytesOrThrow(modified_path);
|
|
1281
|
+
const origText = await extractTextFromBuffer(origBuf, compare_clean);
|
|
1282
|
+
const modText = await extractTextFromBuffer(modBuf, compare_clean);
|
|
1283
|
+
const diff = create_word_patch_diff(
|
|
1284
|
+
origText,
|
|
1285
|
+
modText,
|
|
1286
|
+
basename2(original_path),
|
|
1287
|
+
basename2(modified_path)
|
|
1288
|
+
);
|
|
1289
|
+
return {
|
|
1290
|
+
content: [{ type: "text", text: diff || "No differences found." }]
|
|
1291
|
+
};
|
|
1292
|
+
} catch (e) {
|
|
1293
|
+
return {
|
|
1294
|
+
isError: true,
|
|
1295
|
+
content: [{ type: "text", text: `Error: ${e.message}` }]
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
);
|
|
1300
|
+
server.registerTool(
|
|
1301
|
+
"finalize_document",
|
|
1302
|
+
{
|
|
1303
|
+
description: "Prepares a document for external distribution or e-signature.",
|
|
1304
|
+
inputSchema: {
|
|
1305
|
+
file_path: z.string().describe("Absolute path to the DOCX file."),
|
|
1306
|
+
output_path: z.string().optional().describe("Optional output path."),
|
|
1307
|
+
sanitize_mode: z.enum(["full", "keep-markup"]).optional().describe("full removes all markup, keep-markup redacts metadata."),
|
|
1308
|
+
accept_all: z.boolean().optional().describe(
|
|
1309
|
+
"If true, auto-accepts all unresolved track changes before finalizing."
|
|
1310
|
+
),
|
|
1311
|
+
protection_mode: z.enum(["read_only", "encrypt"]).optional().describe("Native OOXML document locking."),
|
|
1312
|
+
password: z.string().optional().describe("Ignored in this environment."),
|
|
1313
|
+
author: z.string().optional().describe("Replace all remaining markup authorship with this name."),
|
|
1314
|
+
export_pdf: z.boolean().optional().describe("Ignored in this environment.")
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
async ({
|
|
1318
|
+
file_path,
|
|
1319
|
+
output_path,
|
|
1320
|
+
sanitize_mode,
|
|
1321
|
+
accept_all,
|
|
1322
|
+
protection_mode,
|
|
1323
|
+
author,
|
|
1324
|
+
export_pdf
|
|
1325
|
+
}) => {
|
|
1326
|
+
try {
|
|
1327
|
+
let outPath = output_path;
|
|
1328
|
+
if (!outPath) {
|
|
1329
|
+
const ext = extname(file_path);
|
|
1330
|
+
const base = basename2(file_path, ext);
|
|
1331
|
+
const dir = dirname(file_path);
|
|
1332
|
+
outPath = resolve2(dir, `${base}_final${ext}`);
|
|
1003
1333
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1334
|
+
const buf = readFileBytesOrThrow(file_path);
|
|
1335
|
+
const doc = await DocumentObject2.load(buf);
|
|
1336
|
+
const result = await finalize_document(doc, {
|
|
1337
|
+
filename: basename2(file_path),
|
|
1338
|
+
sanitize_mode: sanitize_mode || "full",
|
|
1339
|
+
accept_all,
|
|
1340
|
+
protection_mode,
|
|
1341
|
+
author,
|
|
1342
|
+
export_pdf
|
|
1343
|
+
});
|
|
1344
|
+
fs.writeFileSync(outPath, result.outBuffer);
|
|
1006
1345
|
return {
|
|
1007
1346
|
content: [
|
|
1008
1347
|
{
|
|
1009
1348
|
type: "text",
|
|
1010
|
-
text: `
|
|
1349
|
+
text: `Saved to: ${outPath}
|
|
1350
|
+
|
|
1351
|
+
${result.reportText}`
|
|
1011
1352
|
}
|
|
1012
|
-
]
|
|
1013
|
-
|
|
1353
|
+
]
|
|
1354
|
+
};
|
|
1355
|
+
} catch (e) {
|
|
1356
|
+
return {
|
|
1357
|
+
isError: true,
|
|
1358
|
+
content: [{ type: "text", text: `Error: ${e.message}` }]
|
|
1014
1359
|
};
|
|
1015
1360
|
}
|
|
1016
1361
|
}
|
|
1017
1362
|
);
|
|
1363
|
+
server.registerTool(
|
|
1364
|
+
"login_to_adeu_cloud",
|
|
1365
|
+
{ description: "Logs the user into the Adeu Cloud backend." },
|
|
1366
|
+
async () => {
|
|
1367
|
+
try {
|
|
1368
|
+
return await login_to_adeu_cloud();
|
|
1369
|
+
} catch (e) {
|
|
1370
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
);
|
|
1374
|
+
server.registerTool(
|
|
1375
|
+
"logout_of_adeu_cloud",
|
|
1376
|
+
{ description: "Logs out of the Adeu Cloud backend." },
|
|
1377
|
+
async () => {
|
|
1378
|
+
try {
|
|
1379
|
+
return await logout_of_adeu_cloud();
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
);
|
|
1385
|
+
server.registerTool(
|
|
1386
|
+
"create_email_draft",
|
|
1387
|
+
{
|
|
1388
|
+
description: "Creates an email draft in the user's native draft box (Outlook Drafts or Gmail Drafts).\n\nTWO MODES:\n1. Reply mode: pass `reply_to_email_id` to create a threaded reply. The draft inherits subject, recipients, and threading headers from the original \u2014 do NOT pass `subject` or `to_recipients`.\n2. New email mode: omit `reply_to_email_id` and pass BOTH `subject` and `to_recipients`.\n\n`reply_to_email_id` accepts the same ID formats as search_and_fetch_emails (`msg_*` short IDs, `adeu_*` references, or raw provider IDs). Short IDs are validated against the local cache before the call; stale ones fail fast with a clear error telling you to re-search.\n\n`body_markdown` is converted server-side to styled HTML with inlined CSS for email-client compatibility. Write the body in plain Markdown \u2014 do not pre-render HTML.\n\n`attachment_paths` takes absolute file paths on the user's local disk and uploads them with the draft. Useful right after search_and_fetch_emails downloaded attachments \u2014 those local paths can be passed directly here.",
|
|
1389
|
+
inputSchema: {
|
|
1390
|
+
body_markdown: z.string(),
|
|
1391
|
+
reply_to_email_id: z.string().optional(),
|
|
1392
|
+
subject: z.string().optional(),
|
|
1393
|
+
to_recipients: z.array(z.string()).optional(),
|
|
1394
|
+
attachment_paths: z.array(z.string()).optional(),
|
|
1395
|
+
mailbox_address: z.string().optional().describe(
|
|
1396
|
+
"Optional target mailbox email address to create the draft in."
|
|
1397
|
+
)
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
async (args) => {
|
|
1401
|
+
try {
|
|
1402
|
+
return await create_email_draft(args);
|
|
1403
|
+
} catch (e) {
|
|
1404
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
1408
|
+
server.registerTool(
|
|
1409
|
+
"list_available_mailboxes",
|
|
1410
|
+
{
|
|
1411
|
+
description: "Lists all personal and shared delegated mailboxes the authenticated user has access to. Returns each mailbox's `email_address`, `display_name`, auto-processing settings, and write-back preference.\n\nCall this FIRST when the user mentions a specific mailbox or shared inbox by name, to resolve the canonical `email_address`. Then pass that address as `mailbox_address` to `search_and_fetch_emails` or `create_email_draft` to scope the operation.\n\nOmitting `mailbox_address` on those tools targets the user's primary personal mailbox.",
|
|
1412
|
+
inputSchema: {}
|
|
1413
|
+
},
|
|
1414
|
+
async () => {
|
|
1415
|
+
try {
|
|
1416
|
+
return await list_available_mailboxes();
|
|
1417
|
+
} catch (e) {
|
|
1418
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
);
|
|
1422
|
+
function formatBatchResult(stats, outPath, dry_run) {
|
|
1423
|
+
let res = "";
|
|
1424
|
+
if (dry_run) {
|
|
1425
|
+
res = `Dry-run simulation complete.
|
|
1426
|
+
`;
|
|
1427
|
+
} else {
|
|
1428
|
+
res = `Batch complete. Saved to: ${outPath}
|
|
1429
|
+
`;
|
|
1430
|
+
}
|
|
1431
|
+
res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
|
|
1432
|
+
`;
|
|
1433
|
+
res += `Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.
|
|
1434
|
+
`;
|
|
1435
|
+
if (stats.edits && stats.edits.length > 0) {
|
|
1436
|
+
res += "\nDetailed Edit Reports:\n";
|
|
1437
|
+
for (let i = 0; i < stats.edits.length; i++) {
|
|
1438
|
+
const report = stats.edits[i];
|
|
1439
|
+
const status_indicator = report.status === "applied" ? "\u2705 [applied]" : "\u274C [failed]";
|
|
1440
|
+
res += `Edit ${i + 1} ${status_indicator}:
|
|
1441
|
+
`;
|
|
1442
|
+
res += ` Target: '${report.target_text}'
|
|
1443
|
+
`;
|
|
1444
|
+
res += ` New text: '${report.new_text}'
|
|
1445
|
+
`;
|
|
1446
|
+
if (report.warning) {
|
|
1447
|
+
res += ` Warning: ${report.warning}
|
|
1448
|
+
`;
|
|
1449
|
+
}
|
|
1450
|
+
if (report.error) {
|
|
1451
|
+
res += ` Error: ${report.error}
|
|
1452
|
+
`;
|
|
1453
|
+
}
|
|
1454
|
+
if (report.critic_markup) {
|
|
1455
|
+
res += ` Preview (CriticMarkup): ${report.critic_markup}
|
|
1456
|
+
`;
|
|
1457
|
+
}
|
|
1458
|
+
if (report.clean_text) {
|
|
1459
|
+
res += ` Clean text preview: ${report.clean_text}
|
|
1460
|
+
`;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (stats.skipped_details && stats.skipped_details.length > 0) {
|
|
1465
|
+
res += `
|
|
1466
|
+
|
|
1467
|
+
Skipped Details:
|
|
1468
|
+
${stats.skipped_details.join("\n")}`;
|
|
1469
|
+
}
|
|
1470
|
+
return res;
|
|
1471
|
+
}
|
|
1018
1472
|
async function main() {
|
|
1019
1473
|
const transport = new StdioServerTransport();
|
|
1020
1474
|
await server.connect(transport);
|
|
@@ -1023,4 +1477,7 @@ async function main() {
|
|
|
1023
1477
|
);
|
|
1024
1478
|
}
|
|
1025
1479
|
main().catch(console.error);
|
|
1480
|
+
export {
|
|
1481
|
+
formatBatchResult
|
|
1482
|
+
};
|
|
1026
1483
|
//# sourceMappingURL=index.js.map
|