@adeu/mcp-server 1.6.8 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
- import { readFileSync } from "fs";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { readFileSync as readFileSync3 } from "fs";
8
11
  import { basename as basename2, resolve as resolve2, extname, dirname } from "path";
9
12
  import {
10
13
  identifyEngine,
@@ -12,7 +15,7 @@ import {
12
15
  DocumentObject as DocumentObject2,
13
16
  RedlineEngine,
14
17
  BatchValidationError,
15
- create_unified_diff,
18
+ create_word_patch_diff,
16
19
  finalize_document
17
20
  } from "@adeu/core";
18
21
 
@@ -157,13 +160,457 @@ ${ui_markdown}`;
157
160
  };
158
161
  }
159
162
 
163
+ // src/desktop-auth.ts
164
+ import { createServer } from "http";
165
+ import { exec } from "child_process";
166
+ import { homedir, platform } from "os";
167
+ import { join } from "path";
168
+ import {
169
+ writeFileSync,
170
+ readFileSync,
171
+ mkdirSync,
172
+ existsSync,
173
+ rmSync,
174
+ chmodSync
175
+ } from "fs";
176
+
177
+ // src/shared.ts
178
+ var FRONTEND_URL = process.env.ADEU_FRONTEND_URL || "https://app.adeu.ai";
179
+ var BACKEND_URL = process.env.ADEU_BACKEND_URL || "https://app.adeu.ai";
180
+
181
+ // src/desktop-auth.ts
182
+ var ADEU_DIR = join(homedir(), ".adeu");
183
+ var CRED_PATH = join(ADEU_DIR, "credentials.json");
184
+ function openBrowser(url) {
185
+ if (platform() === "darwin") exec(`open "${url}"`);
186
+ else if (platform() === "win32") exec(`start "" "${url}"`);
187
+ else exec(`xdg-open "${url}"`);
188
+ }
189
+ var DesktopAuthManager = class {
190
+ static getApiKey() {
191
+ if (!existsSync(CRED_PATH)) return null;
192
+ try {
193
+ const data = JSON.parse(readFileSync(CRED_PATH, "utf-8"));
194
+ return data.api_key || null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ static setApiKey(apiKey) {
200
+ if (!existsSync(ADEU_DIR)) {
201
+ mkdirSync(ADEU_DIR, { recursive: true });
202
+ }
203
+ writeFileSync(CRED_PATH, JSON.stringify({ api_key: apiKey }));
204
+ chmodSync(CRED_PATH, 384);
205
+ }
206
+ static clearApiKey() {
207
+ if (existsSync(CRED_PATH)) {
208
+ rmSync(CRED_PATH);
209
+ }
210
+ }
211
+ static async authenticateInteractive() {
212
+ return new Promise((resolve3, reject) => {
213
+ let server2;
214
+ const timeout = setTimeout(
215
+ () => {
216
+ if (server2) server2.close();
217
+ reject(new Error("Authentication timed out after 5 minutes."));
218
+ },
219
+ 5 * 60 * 1e3
220
+ );
221
+ server2 = createServer((req, res) => {
222
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
223
+ if (url.pathname === "/callback") {
224
+ const apiKey = url.searchParams.get("api_key");
225
+ res.writeHead(apiKey ? 200 : 400, { "Content-Type": "text/html" });
226
+ const title = apiKey ? "Authentication Successful!" : "Authentication Failed";
227
+ 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.";
228
+ const color = apiKey ? "#107c10" : "#d83b01";
229
+ res.end(`
230
+ <!DOCTYPE html><html><head><title>${title}</title>
231
+ <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>
232
+ </head><body><div class="container"><h1>${title}</h1><p>${text}</p>
233
+ <script>setTimeout(()=>window.close(), 3000);</script>
234
+ </div></body></html>
235
+ `);
236
+ clearTimeout(timeout);
237
+ setTimeout(() => server2.close(), 100);
238
+ if (apiKey) {
239
+ this.setApiKey(apiKey);
240
+ resolve3(apiKey);
241
+ } else {
242
+ reject(new Error("No API key received in callback."));
243
+ }
244
+ } else {
245
+ res.writeHead(404);
246
+ res.end();
247
+ }
248
+ });
249
+ server2.listen(0, "127.0.0.1", () => {
250
+ const address = server2.address();
251
+ if (address && typeof address !== "string") {
252
+ const authUrl = `${FRONTEND_URL}/login?desktop_port=${address.port}`;
253
+ openBrowser(authUrl);
254
+ }
255
+ });
256
+ });
257
+ }
258
+ static async ensureAuthenticated() {
259
+ const key = this.getApiKey();
260
+ if (key) return key;
261
+ return this.authenticateInteractive();
262
+ }
263
+ };
264
+ async function getCloudAuthToken() {
265
+ const key = DesktopAuthManager.getApiKey();
266
+ if (!key) {
267
+ throw new Error(
268
+ "Authentication Required: You are not logged in. Please call the `login_to_adeu_cloud` tool first to authenticate, then try this task again."
269
+ );
270
+ }
271
+ return key;
272
+ }
273
+
274
+ // src/tools/auth.ts
275
+ async function login_to_adeu_cloud() {
276
+ try {
277
+ const apiKey = await DesktopAuthManager.ensureAuthenticated();
278
+ const res = await fetch(`${BACKEND_URL}/api/v1/auth/me`, {
279
+ headers: {
280
+ Authorization: `Bearer ${apiKey}`,
281
+ Accept: "application/json"
282
+ }
283
+ });
284
+ if (res.status === 401) {
285
+ DesktopAuthManager.clearApiKey();
286
+ throw new Error(
287
+ "Your previous session expired. The stale key has been cleared. Please call `login_to_adeu_cloud` ONE MORE TIME to log in fresh."
288
+ );
289
+ }
290
+ if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
291
+ const data = await res.json();
292
+ return {
293
+ content: [
294
+ {
295
+ type: "text",
296
+ text: `Login successful! Connected to Adeu Cloud as: ${data.email || "Unknown Email"}.`
297
+ }
298
+ ]
299
+ };
300
+ } catch (err) {
301
+ return { isError: true, content: [{ type: "text", text: err.message }] };
302
+ }
303
+ }
304
+ async function logout_of_adeu_cloud() {
305
+ DesktopAuthManager.clearApiKey();
306
+ return {
307
+ content: [
308
+ {
309
+ type: "text",
310
+ text: "Successfully logged out. The local API key has been removed."
311
+ }
312
+ ]
313
+ };
314
+ }
315
+
316
+ // src/tools/email.ts
317
+ import { homedir as homedir2, tmpdir } from "os";
318
+ import { join as join2 } from "path";
319
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
320
+ import { createHash } from "crypto";
321
+ var CACHE_FILE = join2(homedir2(), ".adeu", "mcp_id_cache.json");
322
+ var MAX_CACHE_SIZE = 1e3;
323
+ function loadIdCache() {
324
+ if (existsSync2(CACHE_FILE)) {
325
+ try {
326
+ return JSON.parse(readFileSync2(CACHE_FILE, "utf-8"));
327
+ } catch {
328
+ return {};
329
+ }
330
+ }
331
+ return {};
332
+ }
333
+ function saveIdCache(cache) {
334
+ try {
335
+ mkdirSync2(join2(homedir2(), ".adeu"), { recursive: true });
336
+ const keys = Object.keys(cache);
337
+ if (keys.length > MAX_CACHE_SIZE) {
338
+ const trimmed = {};
339
+ keys.slice(-MAX_CACHE_SIZE).forEach((k) => trimmed[k] = cache[k]);
340
+ cache = trimmed;
341
+ }
342
+ writeFileSync2(CACHE_FILE, JSON.stringify(cache));
343
+ } catch {
344
+ }
345
+ }
346
+ function minifyEmailId(realId, cache) {
347
+ if (!realId) return realId;
348
+ const hash = createHash("md5").update(realId).digest("hex").slice(0, 6);
349
+ const shortId = `msg_${hash}`;
350
+ cache[shortId] = realId;
351
+ return shortId;
352
+ }
353
+ function resolveEmailId(shortId) {
354
+ if (!shortId) return shortId;
355
+ const cache = loadIdCache();
356
+ return cache[shortId] || shortId;
357
+ }
358
+ function stripTags(html) {
359
+ if (!html) return "";
360
+ let text = html.replace(/<(style|script|head)[^>]*>[\s\S]*?<\/\1>/gi, "");
361
+ text = text.replace(
362
+ /<\/?(p|div|br|hr|tr|li|h[1-6]|blockquote)\b[^>]*>/gi,
363
+ "\n"
364
+ );
365
+ text = text.replace(/<[^>]+>/g, "");
366
+ return text.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
367
+ }
368
+ function removeNestedQuotes(text) {
369
+ if (!text) return "";
370
+ const patterns = [
371
+ /_{10,}/m,
372
+ /^From:\s.*?\n(?:.*\n){0,5}?Sent:\s/m,
373
+ /-----Original Message-----/m,
374
+ /On .{1,200}? wrote:/m,
375
+ /^Original Message$/m
376
+ ];
377
+ let earliestCut = text.length;
378
+ for (const pattern of patterns) {
379
+ const match = pattern.exec(text);
380
+ if (match && match.index < earliestCut) {
381
+ earliestCut = match.index;
382
+ }
383
+ }
384
+ return text.substring(0, earliestCut).trim();
385
+ }
386
+ function getUniqueFilepath(saveDir, filename) {
387
+ let filepath = join2(saveDir, filename);
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;
397
+ }
398
+ async function search_and_fetch_emails(args) {
399
+ const apiKey = await getCloudAuthToken();
400
+ const realEmailId = args.email_id ? resolveEmailId(args.email_id) : void 0;
401
+ const payload = {
402
+ email_id: realEmailId,
403
+ sender: args.sender,
404
+ subject: args.subject,
405
+ has_attachments: args.has_attachments,
406
+ attachment_name: args.attachment_name,
407
+ is_unread: args.is_unread,
408
+ days_ago: args.days_ago,
409
+ folder: args.folder,
410
+ limit: args.limit ?? 10,
411
+ offset: args.offset ?? 0
412
+ };
413
+ Object.keys(payload).forEach(
414
+ (k) => payload[k] === void 0 && delete payload[k]
415
+ );
416
+ const res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
417
+ method: "POST",
418
+ headers: {
419
+ Authorization: `Bearer ${apiKey}`,
420
+ "Content-Type": "application/json"
421
+ },
422
+ body: JSON.stringify(payload)
423
+ });
424
+ if (res.status === 401) {
425
+ DesktopAuthManager.clearApiKey();
426
+ throw new Error(
427
+ "Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
428
+ );
429
+ }
430
+ if (!res.ok) throw new Error(`Cloud search failed: ${await res.text()}`);
431
+ const data = await res.json();
432
+ const cache = loadIdCache();
433
+ if (data.type === "previews") {
434
+ const previews = data.previews || [];
435
+ if (!previews.length)
436
+ return {
437
+ content: [
438
+ {
439
+ type: "text",
440
+ text: "No emails found matching your search criteria."
441
+ }
442
+ ]
443
+ };
444
+ const lines = [
445
+ `Found ${previews.length} email(s). Here are the previews:`,
446
+ ""
447
+ ];
448
+ for (const p of previews) {
449
+ const shortId = minifyEmailId(p.id, cache);
450
+ const attFlag = p.has_attachments ? "\u{1F4CE} (Has Attachments)" : "";
451
+ const unreadFlag = p.is_read === false ? "\u{1F7E2} [UNREAD]" : "";
452
+ lines.push(
453
+ `- **ID**: \`${shortId}\`
454
+ **Subject**: ${p.subject} ${attFlag} ${unreadFlag}
455
+ **From**: ${p.sender_name} <${p.sender_email}>
456
+ **Date**: ${p.received_datetime}
457
+ **Preview**: ${p.preview_text}
458
+ `
459
+ );
460
+ }
461
+ saveIdCache(cache);
462
+ 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`."
464
+ );
465
+ return { content: [{ type: "text", text: lines.join("\n") }] };
466
+ }
467
+ if (data.type === "full_email") {
468
+ const full = data.full_email || {};
469
+ const shortTargetId = minifyEmailId(full.id || "unknown_id", cache);
470
+ saveIdCache(cache);
471
+ const baseDir = args.working_directory && existsSync2(args.working_directory) ? args.working_directory : tmpdir();
472
+ const saveDir = join2(
473
+ baseDir,
474
+ args.working_directory ? "adeu_attachments" : "adeu_downloads",
475
+ shortTargetId
476
+ );
477
+ mkdirSync2(saveDir, { recursive: true });
478
+ async function processAttachments(msg) {
479
+ const localFiles = [];
480
+ for (const att of msg.attachments || []) {
481
+ if (att.base64_data) {
482
+ try {
483
+ const filepath = getUniqueFilepath(
484
+ saveDir,
485
+ att.filename || "unnamed_file"
486
+ );
487
+ writeFileSync2(filepath, Buffer.from(att.base64_data, "base64"));
488
+ localFiles.push(filepath);
489
+ delete att.base64_data;
490
+ } catch (e) {
491
+ console.error(`Failed to save attachment ${att.filename}`, e);
492
+ }
493
+ }
494
+ }
495
+ return localFiles;
496
+ }
497
+ const targetFiles = await processAttachments(full);
498
+ const lines = [
499
+ `# Email Thread: ${full.subject}`,
500
+ "",
501
+ "## Target Message (Newest):",
502
+ `**From**: ${full.sender_name} <${full.sender_email}>`,
503
+ `**Date**: ${full.received_datetime}`
504
+ ];
505
+ if (targetFiles.length) {
506
+ lines.push("**Attachments Saved Locally**:");
507
+ targetFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
508
+ }
509
+ const cleanBody = removeNestedQuotes(stripTags(full.body_html || ""));
510
+ lines.push(`**Body**:
511
+ \`\`\`
512
+ ${cleanBody}
513
+ \`\`\`
514
+ `);
515
+ if (full.is_thread && full.messages?.length) {
516
+ lines.push("## Previous Messages in Thread (Historical Context):");
517
+ for (let i = 0; i < full.messages.length; i++) {
518
+ const histMsg = full.messages[i];
519
+ const histFiles = await processAttachments(histMsg);
520
+ lines.push(
521
+ `### Message -${i + 1} (Older)
522
+ **From**: ${histMsg.sender_name} <${histMsg.sender_email}>
523
+ **Date**: ${histMsg.received_datetime}`
524
+ );
525
+ if (histFiles.length) {
526
+ lines.push("**Attachments Saved Locally**:");
527
+ histFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
528
+ }
529
+ lines.push(
530
+ `**Body**:
531
+ \`\`\`
532
+ ${removeNestedQuotes(stripTags(histMsg.body_html || ""))}
533
+ \`\`\`
534
+ `
535
+ );
536
+ }
537
+ }
538
+ return { content: [{ type: "text", text: lines.join("\n") }] };
539
+ }
540
+ return {
541
+ isError: true,
542
+ content: [{ type: "text", text: "Unknown response format from backend." }]
543
+ };
544
+ }
545
+ async function create_email_draft(args) {
546
+ const apiKey = await getCloudAuthToken();
547
+ if (!args.reply_to_email_id && (!args.subject || !args.to_recipients)) {
548
+ throw new Error(
549
+ "You must provide either 'reply_to_email_id' OR both 'subject' and 'to_recipients'."
550
+ );
551
+ }
552
+ const formData = new FormData();
553
+ formData.append("body_markdown", args.body_markdown);
554
+ if (args.reply_to_email_id) {
555
+ formData.append(
556
+ "reply_to_email_id",
557
+ resolveEmailId(args.reply_to_email_id)
558
+ );
559
+ }
560
+ if (args.subject) formData.append("subject", args.subject);
561
+ if (args.to_recipients) {
562
+ const recips = typeof args.to_recipients === "string" ? JSON.parse(args.to_recipients) : args.to_recipients;
563
+ formData.append("to_recipients", JSON.stringify(recips));
564
+ }
565
+ if (args.attachment_paths) {
566
+ const paths = typeof args.attachment_paths === "string" ? JSON.parse(args.attachment_paths) : args.attachment_paths;
567
+ for (const p of paths) {
568
+ const buf = readFileSync2(p);
569
+ const filename = p.split(/[/\\]/).pop();
570
+ formData.append("files", new Blob([buf]), filename);
571
+ }
572
+ }
573
+ const res = await fetch(`${BACKEND_URL}/api/v1/emails/drafts/new`, {
574
+ method: "POST",
575
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
576
+ body: formData
577
+ });
578
+ if (res.status === 401) {
579
+ DesktopAuthManager.clearApiKey();
580
+ throw new Error(
581
+ "Authentication expired. Please call `login_to_adeu_cloud`."
582
+ );
583
+ }
584
+ if (!res.ok)
585
+ throw new Error(`Cloud draft creation failed: ${await res.text()}`);
586
+ const data = await res.json();
587
+ return {
588
+ content: [
589
+ {
590
+ type: "text",
591
+ text: `Successfully created email draft! Draft ID: ${data.id}`
592
+ }
593
+ ]
594
+ };
595
+ }
596
+
160
597
  // src/index.ts
598
+ function readFileBytesOrThrow(filePath) {
599
+ try {
600
+ return readFileSync3(filePath);
601
+ } catch (err) {
602
+ if (err.code === "ENOENT") {
603
+ throw new Error(`File not found: ${filePath}`);
604
+ }
605
+ throw err;
606
+ }
607
+ }
161
608
  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";
162
609
  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.";
163
610
  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";
164
611
  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.";
165
612
  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.";
166
- var server = new Server(
613
+ var server = new Server2(
167
614
  {
168
615
  name: "adeu-redlining-service",
169
616
  version: "1.0.0"
@@ -183,12 +630,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
183
630
  inputSchema: {
184
631
  type: "object",
185
632
  properties: {
186
- file_path: { type: "string", description: "Absolute path to the DOCX file." },
187
- clean_view: { type: "boolean", description: "If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text.", default: false },
188
- mode: { type: "string", enum: ["full", "outline", "appendix"], description: "'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms.", default: "full" },
189
- page: { type: "number", description: "Page number (1-indexed) for mode='full'. Defaults to 1.", default: 1 },
190
- outline_max_level: { type: "number", description: "For mode='outline' only: cap on heading depth.", default: 2 },
191
- outline_verbose: { type: "boolean", description: "For mode='outline' only: includes metadata.", default: false }
633
+ file_path: {
634
+ type: "string",
635
+ description: "Absolute path to the DOCX file."
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
+ }
192
663
  },
193
664
  required: ["file_path"]
194
665
  }
@@ -199,14 +670,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
199
670
  inputSchema: {
200
671
  type: "object",
201
672
  properties: {
202
- original_docx_path: { type: "string", description: "Absolute path to the source file." },
203
- author_name: { type: "string", description: "Name to appear in Track Changes (e.g., 'Reviewer AI')." },
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
+ },
204
681
  changes: {
205
682
  type: "array",
206
683
  description: "List of changes to apply. Each change must specify 'type'.",
207
684
  items: { type: "object" }
208
685
  },
209
- output_path: { type: "string", description: "Optional output path." }
686
+ output_path: {
687
+ type: "string",
688
+ description: "Optional output path."
689
+ }
210
690
  },
211
691
  required: ["original_docx_path", "author_name", "changes"]
212
692
  }
@@ -217,8 +697,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
217
697
  inputSchema: {
218
698
  type: "object",
219
699
  properties: {
220
- docx_path: { type: "string", description: "Absolute path to the DOCX file." },
221
- output_path: { type: "string", description: "Optional output path." }
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
+ }
222
708
  },
223
709
  required: ["docx_path"]
224
710
  }
@@ -229,8 +715,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
229
715
  inputSchema: {
230
716
  type: "object",
231
717
  properties: {
232
- original_path: { type: "string", description: "Absolute path to the baseline DOCX file." },
233
- modified_path: { type: "string", description: "Absolute path to the modified DOCX file." }
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
+ }
234
731
  },
235
732
  required: ["original_path", "modified_path"]
236
733
  }
@@ -241,157 +738,289 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
241
738
  inputSchema: {
242
739
  type: "object",
243
740
  properties: {
244
- file_path: { type: "string", description: "Absolute path to the DOCX file." },
245
- output_path: { type: "string", description: "Optional output path." },
246
- sanitize_mode: { type: "string", enum: ["full", "keep-markup"], description: "full removes all markup, keep-markup redacts metadata but keeps comments/redlines." },
247
- accept_all: { type: "boolean", description: "If true, auto-accepts all unresolved track changes before finalizing." },
248
- protection_mode: { type: "string", enum: ["read_only", "encrypt"], description: "Native OOXML document locking. encrypt falls back to read_only in this environment." },
249
- password: { type: "string", description: "Ignored in this environment." },
250
- author: { type: "string", description: "Replace all remaining markup authorship with this name." },
251
- export_pdf: { type: "boolean", description: "Ignored in this environment." }
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
+ }
252
775
  },
253
776
  required: ["file_path"]
254
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
+ }
255
823
  }
256
824
  ]
257
825
  };
258
826
  });
259
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
260
- const { name, arguments: args } = request.params;
261
- try {
262
- if (name === "read_docx") {
263
- const filePath = args?.file_path;
264
- const cleanView = args?.clean_view ?? false;
265
- const mode = args?.mode ?? "full";
266
- const page = args?.page ?? 1;
267
- const outline_max_level = args?.outline_max_level ?? 2;
268
- const outline_verbose = args?.outline_verbose ?? false;
269
- const buf = readFileSync(filePath);
270
- const text = await extractTextFromBuffer(buf, cleanView);
271
- if (mode === "outline") {
272
- const doc = await DocumentObject2.load(buf);
273
- return build_outline_response(doc, text, filePath, outline_max_level, outline_verbose);
274
- }
275
- if (mode === "appendix") {
276
- return build_appendix_response(text, page, filePath);
277
- }
278
- return build_paginated_response(text, page, filePath);
279
- }
280
- if (name === "process_document_batch") {
281
- const origPath = args?.original_docx_path;
282
- const authorName = args?.author_name;
283
- const changes = args?.changes;
284
- let outPath = args?.output_path;
285
- if (!outPath) {
286
- const ext = extname(origPath);
287
- const base = basename2(origPath, ext);
288
- const dir = dirname(origPath);
289
- outPath = resolve2(dir, `${base}_processed${ext}`);
827
+ server.setRequestHandler(
828
+ CallToolRequestSchema,
829
+ async (request) => {
830
+ const { name, arguments: args } = request.params;
831
+ try {
832
+ if (name === "read_docx") {
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);
290
855
  }
291
- const buf = readFileSync(origPath);
292
- const doc = await DocumentObject2.load(buf);
293
- const engine = new RedlineEngine(doc, authorName);
294
- let stats;
295
- try {
296
- stats = engine.process_batch(changes);
297
- } catch (e) {
298
- if (e instanceof BatchValidationError) {
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()) {
299
862
  return {
300
- content: [{ type: "text", text: `Batch rejected. Some edits failed validation:
301
-
302
- ${e.errors.join("\n\n")}` }],
303
- isError: true
863
+ content: [
864
+ { type: "text", text: "Error: author_name cannot be empty." }
865
+ ]
304
866
  };
305
867
  }
306
- throw e;
307
- }
308
- const outBuf = await doc.save();
309
- const fs = await import("fs");
310
- fs.writeFileSync(outPath, outBuf);
311
- let res = `Batch complete. Saved to: ${outPath}
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}
312
905
  Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
313
906
  Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.`;
314
- if (stats.skipped_details?.length > 0) {
315
- res += `
907
+ if (stats.skipped_details?.length > 0) {
908
+ res += `
316
909
 
317
910
  Skipped Details:
318
911
  ${stats.skipped_details.join("\n")}`;
912
+ }
913
+ return {
914
+ content: [{ type: "text", text: res }]
915
+ };
319
916
  }
320
- return {
321
- content: [{ type: "text", text: res }]
322
- };
323
- }
324
- if (name === "accept_all_changes") {
325
- const docxPath = args?.docx_path;
326
- let outPath = args?.output_path;
327
- if (!outPath) {
328
- const ext = extname(docxPath);
329
- const base = basename2(docxPath, ext);
330
- const dir = dirname(docxPath);
331
- outPath = resolve2(dir, `${base}_clean${ext}`);
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);
933
+ return {
934
+ content: [
935
+ {
936
+ type: "text",
937
+ text: `Accepted all changes. Saved to: ${outPath}`
938
+ }
939
+ ]
940
+ };
332
941
  }
333
- const buf = readFileSync(docxPath);
334
- const doc = await DocumentObject2.load(buf);
335
- const engine = new RedlineEngine(doc);
336
- engine.accept_all_revisions();
337
- const outBuf = await doc.save();
338
- const fs = await import("fs");
339
- fs.writeFileSync(outPath, outBuf);
340
- return {
341
- content: [{ type: "text", text: `Accepted all changes. Saved to: ${outPath}` }]
342
- };
343
- }
344
- if (name === "diff_docx_files") {
345
- const origPath = args?.original_path;
346
- const modPath = args?.modified_path;
347
- const origBuf = readFileSync(origPath);
348
- const modBuf = readFileSync(modPath);
349
- const origText = await extractTextFromBuffer(origBuf, true);
350
- const modText = await extractTextFromBuffer(modBuf, true);
351
- const diff = create_unified_diff(origText, modText);
352
- return {
353
- content: [{ type: "text", text: diff || "No differences found." }]
354
- };
355
- }
356
- if (name === "finalize_document") {
357
- const filePath = args?.file_path;
358
- let outPath = args?.output_path;
359
- if (!outPath) {
360
- const ext = extname(filePath);
361
- const base = basename2(filePath, ext);
362
- const dir = dirname(filePath);
363
- outPath = resolve2(dir, `${base}_final${ext}`);
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
+ );
956
+ return {
957
+ content: [{ type: "text", text: diff || "No differences found." }]
958
+ };
364
959
  }
365
- const buf = readFileSync(filePath);
366
- const doc = await DocumentObject2.load(buf);
367
- const result = await finalize_document(doc, {
368
- filename: basename2(filePath),
369
- sanitize_mode: args?.sanitize_mode || "full",
370
- accept_all: args?.accept_all,
371
- protection_mode: args?.protection_mode,
372
- author: args?.author,
373
- export_pdf: args?.export_pdf
374
- });
375
- const fs = await import("fs");
376
- fs.writeFileSync(outPath, result.outBuffer);
377
- return {
378
- content: [{ type: "text", text: `Saved to: ${outPath}
960
+ if (name === "finalize_document") {
961
+ const filePath = args?.file_path;
962
+ let outPath = args?.output_path;
963
+ if (!outPath) {
964
+ const ext = extname(filePath);
965
+ const base = basename2(filePath, ext);
966
+ const dir = dirname(filePath);
967
+ outPath = resolve2(dir, `${base}_final${ext}`);
968
+ }
969
+ const buf = readFileBytesOrThrow(filePath);
970
+ const doc = await DocumentObject2.load(buf);
971
+ const result = await finalize_document(doc, {
972
+ filename: basename2(filePath),
973
+ sanitize_mode: args?.sanitize_mode || "full",
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}
379
986
 
380
- ${result.reportText}` }]
987
+ ${result.reportText}`
988
+ }
989
+ ]
990
+ };
991
+ }
992
+ if (name === "login_to_adeu_cloud") {
993
+ return await login_to_adeu_cloud();
994
+ }
995
+ if (name === "logout_of_adeu_cloud") {
996
+ return await logout_of_adeu_cloud();
997
+ }
998
+ if (name === "search_and_fetch_emails") {
999
+ return await search_and_fetch_emails(args || {});
1000
+ }
1001
+ if (name === "create_email_draft") {
1002
+ return await create_email_draft(args || {});
1003
+ }
1004
+ throw new Error(`Unknown tool: ${name}`);
1005
+ } catch (error) {
1006
+ return {
1007
+ content: [
1008
+ {
1009
+ type: "text",
1010
+ text: `Error executing tool ${name}: ${error.message}`
1011
+ }
1012
+ ],
1013
+ isError: true
381
1014
  };
382
1015
  }
383
- throw new Error(`Unknown tool: ${name}`);
384
- } catch (error) {
385
- return {
386
- content: [{ type: "text", text: `Error executing tool ${name}: ${error.message}` }],
387
- isError: true
388
- };
389
1016
  }
390
- });
1017
+ );
391
1018
  async function main() {
392
1019
  const transport = new StdioServerTransport();
393
1020
  await server.connect(transport);
394
- console.error(`Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio`);
1021
+ console.error(
1022
+ `Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio`
1023
+ );
395
1024
  }
396
1025
  main().catch(console.error);
397
1026
  //# sourceMappingURL=index.js.map