@adeu/mcp-server 1.7.5 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
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
- CallToolRequestSchema,
8
- ListToolsRequestSchema
9
- } from "@modelcontextprotocol/sdk/types.js";
10
- import { readFileSync as readFileSync3 } from "fs";
11
- import { basename as basename2, resolve as resolve2, extname, dirname } from "path";
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) meta_parts.push("fn:" + node.footnote_ids.join(","));
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(`Page ${page} out of range (doc has ${result.total_pages} pages).`);
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(selected.page, selected.total_pages, selected.has_next);
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(nodes, outline_max_level, outline_verbose);
106
- const visible_count = nodes.filter((n) => n.level <= outline_max_level).length;
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(`Appendix page ${page} out of range (appendix has ${result.total_pages} pages).`);
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");
@@ -408,7 +449,8 @@ async function search_and_fetch_emails(args) {
408
449
  days_ago: args.days_ago,
409
450
  folder: args.folder,
410
451
  limit: args.limit ?? 10,
411
- offset: args.offset ?? 0
452
+ offset: args.offset ?? 0,
453
+ mailbox_address: args.mailbox_address
412
454
  };
413
455
  Object.keys(payload).forEach(
414
456
  (k) => payload[k] === void 0 && delete payload[k]
@@ -462,7 +504,10 @@ async function search_and_fetch_emails(args) {
462
504
  lines.push(
463
505
  "\u26A0\uFE0F **ACTION REQUIRED**: To read the full body of an email and download its attachments, call this tool again and provide the exact `email_id`."
464
506
  );
465
- return { content: [{ type: "text", text: lines.join("\n") }] };
507
+ return {
508
+ content: [{ type: "text", text: lines.join("\n") }],
509
+ structuredContent: data
510
+ };
466
511
  }
467
512
  if (data.type === "full_email") {
468
513
  const full = data.full_email || {};
@@ -535,7 +580,10 @@ ${removeNestedQuotes(stripTags(histMsg.body_html || ""))}
535
580
  );
536
581
  }
537
582
  }
538
- return { content: [{ type: "text", text: lines.join("\n") }] };
583
+ return {
584
+ content: [{ type: "text", text: lines.join("\n") }],
585
+ structuredContent: data
586
+ };
539
587
  }
540
588
  return {
541
589
  isError: true,
@@ -558,6 +606,9 @@ async function create_email_draft(args) {
558
606
  );
559
607
  }
560
608
  if (args.subject) formData.append("subject", args.subject);
609
+ if (args.mailbox_address) {
610
+ formData.append("mailbox_address", args.mailbox_address);
611
+ }
561
612
  if (args.to_recipients) {
562
613
  const recips = typeof args.to_recipients === "string" ? JSON.parse(args.to_recipients) : args.to_recipients;
563
614
  formData.append("to_recipients", JSON.stringify(recips));
@@ -593,6 +644,52 @@ async function create_email_draft(args) {
593
644
  ]
594
645
  };
595
646
  }
647
+ async function list_available_mailboxes() {
648
+ const apiKey = await getCloudAuthToken();
649
+ const res = await fetch(`${BACKEND_URL}/api/v1/users/me/shared-mailboxes`, {
650
+ method: "GET",
651
+ headers: {
652
+ Authorization: `Bearer ${apiKey}`,
653
+ Accept: "application/json"
654
+ }
655
+ });
656
+ if (res.status === 401) {
657
+ DesktopAuthManager.clearApiKey();
658
+ throw new Error(
659
+ "Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
660
+ );
661
+ }
662
+ if (!res.ok) {
663
+ throw new Error(`Failed to list available mailboxes: ${await res.text()}`);
664
+ }
665
+ const mailboxes = await res.json();
666
+ if (!mailboxes.length) {
667
+ return {
668
+ content: [
669
+ {
670
+ type: "text",
671
+ text: "No configured mailboxes found for your profile."
672
+ }
673
+ ]
674
+ };
675
+ }
676
+ const lines = [
677
+ "### Connected Mailboxes",
678
+ "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:",
679
+ ""
680
+ ];
681
+ for (const box of mailboxes) {
682
+ lines.push(
683
+ `- **${box.display_name || "Personal Mailbox"}**
684
+ - **Email Address**: \`${box.email_address}\`
685
+ - **Auto-Processing**: ${box.auto_process_enabled ? "Enabled" : "Disabled"}
686
+ - **Write-Back Mode**: \`${box.write_back_preference}\``
687
+ );
688
+ }
689
+ return {
690
+ content: [{ type: "text", text: lines.join("\n") }]
691
+ };
692
+ }
596
693
 
597
694
  // src/index.ts
598
695
  function readFileBytesOrThrow(filePath) {
@@ -605,413 +702,444 @@ function readFileBytesOrThrow(filePath) {
605
702
  throw err;
606
703
  }
607
704
  }
705
+ var DIST_DIR = import.meta.dirname;
706
+ function getAssetContent(folder, filename, fallbackMessage) {
707
+ const filePath = join3(DIST_DIR, folder, filename);
708
+ if (existsSync3(filePath)) {
709
+ return readFileSync3(filePath, "utf-8");
710
+ }
711
+ return fallbackMessage;
712
+ }
608
713
  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
714
  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
715
  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
716
  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
717
  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 Server2(
718
+ var server = new McpServer({
719
+ name: "adeu-redlining-service",
720
+ version: "1.0.0"
721
+ });
722
+ var UI_CSP = {
723
+ connectDomains: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
724
+ resourceDomains: [
725
+ "https://fonts.googleapis.com",
726
+ "https://fonts.gstatic.com"
727
+ ]
728
+ };
729
+ registerAppResource(
730
+ server,
731
+ MARKDOWN_UI_URI,
732
+ MARKDOWN_UI_URI,
733
+ { mimeType: RESOURCE_MIME_TYPE, description: "Adeu Markdown Viewer UI" },
734
+ async () => {
735
+ let html = getAssetContent(
736
+ "templates",
737
+ "markdown_ui.html",
738
+ "<html><body>UI Template Not Found</body></html>"
739
+ );
740
+ const markedJs = getAssetContent(
741
+ "assets",
742
+ "marked.min.js",
743
+ "window.__MARKED_ERROR = 'marked.min.js not found';"
744
+ );
745
+ const svg = getAssetContent("assets", "adeu.svg", "");
746
+ html = html.replace("[[marked_js_code | safe]]", markedJs).replace("[[ adeu_svg_code ]]", svg);
747
+ return {
748
+ contents: [
749
+ {
750
+ uri: MARKDOWN_UI_URI,
751
+ mimeType: RESOURCE_MIME_TYPE,
752
+ text: html,
753
+ _meta: { ui: { csp: UI_CSP } }
754
+ }
755
+ ]
756
+ };
757
+ }
758
+ );
759
+ registerAppResource(
760
+ server,
761
+ EMAIL_UI_URI,
762
+ EMAIL_UI_URI,
763
+ { mimeType: RESOURCE_MIME_TYPE, description: "Adeu Email Viewer UI" },
764
+ async () => {
765
+ let html = getAssetContent(
766
+ "templates",
767
+ "email_ui.html",
768
+ "<html><body>UI Template Not Found</body></html>"
769
+ );
770
+ const svg = getAssetContent("assets", "adeu.svg", "");
771
+ html = html.replace("[[ adeu_svg_code ]]", svg);
772
+ return {
773
+ contents: [
774
+ {
775
+ uri: EMAIL_UI_URI,
776
+ mimeType: RESOURCE_MIME_TYPE,
777
+ text: html,
778
+ _meta: { ui: { csp: UI_CSP } }
779
+ }
780
+ ]
781
+ };
782
+ }
783
+ );
784
+ registerAppTool(
785
+ server,
786
+ "read_docx",
614
787
  {
615
- name: "adeu-redlining-service",
616
- version: "1.0.0"
788
+ title: "Read DOCX",
789
+ description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,
790
+ inputSchema: z.object({
791
+ file_path: z.string().describe("Absolute path to the DOCX file."),
792
+ clean_view: z.boolean().default(false).describe(
793
+ "If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text."
794
+ ),
795
+ mode: z.enum(["full", "outline", "appendix"]).default("full").describe(
796
+ "'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms."
797
+ ),
798
+ page: z.number().default(1).describe("Page number (1-indexed) for mode='full'. Defaults to 1."),
799
+ outline_max_level: z.number().default(2).describe("For mode='outline' only: cap on heading depth."),
800
+ outline_verbose: z.boolean().default(false).describe("For mode='outline' only: includes metadata.")
801
+ }),
802
+ _meta: { ui: { resourceUri: MARKDOWN_UI_URI } }
617
803
  },
618
- {
619
- capabilities: {
620
- tools: {}
804
+ async ({
805
+ file_path,
806
+ clean_view,
807
+ mode,
808
+ page,
809
+ outline_max_level,
810
+ outline_verbose
811
+ }) => {
812
+ try {
813
+ const buf = readFileBytesOrThrow(file_path);
814
+ const text = await extractTextFromBuffer(buf, clean_view);
815
+ if (mode === "outline") {
816
+ const doc = await DocumentObject2.load(buf);
817
+ return build_outline_response(
818
+ doc,
819
+ text,
820
+ file_path,
821
+ outline_max_level,
822
+ outline_verbose
823
+ );
824
+ }
825
+ if (mode === "appendix") {
826
+ return build_appendix_response(text, page, file_path);
827
+ }
828
+ return build_paginated_response(text, page, file_path);
829
+ } catch (e) {
830
+ return {
831
+ isError: true,
832
+ content: [
833
+ {
834
+ type: "text",
835
+ text: `Error executing tool read_docx: ${e.message}`
836
+ }
837
+ ]
838
+ };
621
839
  }
622
840
  }
623
841
  );
624
- server.setRequestHandler(ListToolsRequestSchema, async () => {
625
- return {
626
- tools: [
627
- {
628
- name: "read_docx",
629
- description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,
630
- inputSchema: {
631
- type: "object",
632
- properties: {
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
- }
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" }
842
+ registerAppTool(
843
+ server,
844
+ "search_and_fetch_emails",
845
+ {
846
+ title: "Search & Fetch Emails",
847
+ description: "Searches the user's live email inbox. Returns previews. Call again with `email_id` to fetch the full body.",
848
+ inputSchema: z.object({
849
+ sender: z.string().optional(),
850
+ subject: z.string().optional(),
851
+ has_attachments: z.boolean().optional(),
852
+ attachment_name: z.string().optional(),
853
+ is_unread: z.boolean().optional(),
854
+ days_ago: z.number().optional(),
855
+ folder: z.enum(["inbox", "sent", "all"]).optional(),
856
+ limit: z.number().default(10),
857
+ offset: z.number().default(0),
858
+ email_id: z.string().optional(),
859
+ working_directory: z.string().optional(),
860
+ mailbox_address: z.string().optional().describe("Optional target mailbox email address to search within.")
861
+ }),
862
+ _meta: { ui: { resourceUri: EMAIL_UI_URI } }
863
+ },
864
+ async (args) => {
865
+ try {
866
+ return await search_and_fetch_emails(args);
867
+ } catch (e) {
868
+ return {
869
+ isError: true,
870
+ content: [
871
+ {
872
+ type: "text",
873
+ text: `Error executing tool search_and_fetch_emails: ${e.message}`
806
874
  }
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;
875
+ ]
876
+ };
877
+ }
878
+ }
879
+ );
880
+ server.registerTool(
881
+ "process_document_batch",
882
+ {
883
+ description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,
884
+ inputSchema: {
885
+ original_docx_path: z.string().describe("Absolute path to the source file."),
886
+ author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
887
+ changes: z.array(z.any()).describe("List of changes to apply. Each change must specify 'type'."),
888
+ output_path: z.string().optional().describe("Optional output path.")
889
+ }
890
+ },
891
+ async ({ original_docx_path, author_name, changes, output_path }) => {
831
892
  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);
893
+ if (!author_name || !author_name.trim())
894
+ return {
895
+ content: [
896
+ { type: "text", text: "Error: author_name cannot be empty." }
897
+ ]
898
+ };
899
+ if (!changes || changes.length === 0)
900
+ return {
901
+ content: [{ type: "text", text: "Error: No changes provided." }]
902
+ };
903
+ let outPath = output_path;
904
+ if (!outPath) {
905
+ const ext = extname(original_docx_path);
906
+ const base = basename2(original_docx_path, ext);
907
+ const dir = dirname(original_docx_path);
908
+ outPath = resolve2(dir, `${base}_processed${ext}`);
855
909
  }
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()) {
910
+ const buf = readFileBytesOrThrow(original_docx_path);
911
+ const doc = await DocumentObject2.load(buf);
912
+ const engine = new RedlineEngine(doc, author_name);
913
+ let stats;
914
+ try {
915
+ stats = engine.process_batch(changes);
916
+ } catch (e) {
917
+ if (e instanceof BatchValidationError) {
862
918
  return {
919
+ isError: true,
863
920
  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:
921
+ {
922
+ type: "text",
923
+ text: `Batch rejected. Some edits failed validation:
892
924
 
893
925
  ${e.errors.join("\n\n")}`
894
- }
895
- ],
896
- isError: true
897
- };
898
- }
899
- throw e;
926
+ }
927
+ ]
928
+ };
900
929
  }
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}
930
+ throw e;
931
+ }
932
+ const outBuf = await doc.save();
933
+ fs.writeFileSync(outPath, outBuf);
934
+ let res = `Batch complete. Saved to: ${outPath}
905
935
  Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
906
936
  Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.`;
907
- if (stats.skipped_details?.length > 0) {
908
- res += `
937
+ if (stats.skipped_details?.length > 0) {
938
+ res += `
909
939
 
910
940
  Skipped Details:
911
941
  ${stats.skipped_details.join("\n")}`;
912
- }
913
- return {
914
- content: [{ type: "text", text: res }]
915
- };
916
942
  }
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
- };
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
- );
956
- return {
957
- content: [{ type: "text", text: diff || "No differences found." }]
958
- };
959
- }
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}
986
-
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 || {});
943
+ return { content: [{ type: "text", text: res }] };
944
+ } catch (e) {
945
+ return {
946
+ isError: true,
947
+ content: [{ type: "text", text: `Error: ${e.message}` }]
948
+ };
949
+ }
950
+ }
951
+ );
952
+ server.registerTool(
953
+ "accept_all_changes",
954
+ {
955
+ description: "Accepts all tracked changes and removes all comments in a single operation.",
956
+ inputSchema: {
957
+ docx_path: z.string().describe("Absolute path to the DOCX file."),
958
+ output_path: z.string().optional().describe("Optional output path.")
959
+ }
960
+ },
961
+ async ({ docx_path, output_path }) => {
962
+ try {
963
+ let outPath = output_path;
964
+ if (!outPath) {
965
+ const ext = extname(docx_path);
966
+ const base = basename2(docx_path, ext);
967
+ const dir = dirname(docx_path);
968
+ outPath = resolve2(dir, `${base}_clean${ext}`);
1000
969
  }
1001
- if (name === "create_email_draft") {
1002
- return await create_email_draft(args || {});
970
+ const buf = readFileBytesOrThrow(docx_path);
971
+ const doc = await DocumentObject2.load(buf);
972
+ const engine = new RedlineEngine(doc);
973
+ engine.accept_all_revisions();
974
+ const outBuf = await doc.save();
975
+ fs.writeFileSync(outPath, outBuf);
976
+ return {
977
+ content: [
978
+ { type: "text", text: `Accepted all changes. Saved to: ${outPath}` }
979
+ ]
980
+ };
981
+ } catch (e) {
982
+ return {
983
+ isError: true,
984
+ content: [{ type: "text", text: `Error: ${e.message}` }]
985
+ };
986
+ }
987
+ }
988
+ );
989
+ server.registerTool(
990
+ "diff_docx_files",
991
+ {
992
+ description: DIFF_DOCX_DESC,
993
+ inputSchema: {
994
+ original_path: z.string().describe("Absolute path to the baseline DOCX file."),
995
+ modified_path: z.string().describe("Absolute path to the modified DOCX file."),
996
+ compare_clean: z.boolean().default(true).describe(
997
+ "If True, compares 'Accepted' state. If False, compares raw text."
998
+ )
999
+ }
1000
+ },
1001
+ async ({ original_path, modified_path, compare_clean }) => {
1002
+ try {
1003
+ const origBuf = readFileBytesOrThrow(original_path);
1004
+ const modBuf = readFileBytesOrThrow(modified_path);
1005
+ const origText = await extractTextFromBuffer(origBuf, compare_clean);
1006
+ const modText = await extractTextFromBuffer(modBuf, compare_clean);
1007
+ const diff = create_word_patch_diff(
1008
+ origText,
1009
+ modText,
1010
+ basename2(original_path),
1011
+ basename2(modified_path)
1012
+ );
1013
+ return {
1014
+ content: [{ type: "text", text: diff || "No differences found." }]
1015
+ };
1016
+ } catch (e) {
1017
+ return {
1018
+ isError: true,
1019
+ content: [{ type: "text", text: `Error: ${e.message}` }]
1020
+ };
1021
+ }
1022
+ }
1023
+ );
1024
+ server.registerTool(
1025
+ "finalize_document",
1026
+ {
1027
+ description: "Prepares a document for external distribution or e-signature.",
1028
+ inputSchema: {
1029
+ file_path: z.string().describe("Absolute path to the DOCX file."),
1030
+ output_path: z.string().optional().describe("Optional output path."),
1031
+ sanitize_mode: z.enum(["full", "keep-markup"]).optional().describe("full removes all markup, keep-markup redacts metadata."),
1032
+ accept_all: z.boolean().optional().describe(
1033
+ "If true, auto-accepts all unresolved track changes before finalizing."
1034
+ ),
1035
+ protection_mode: z.enum(["read_only", "encrypt"]).optional().describe("Native OOXML document locking."),
1036
+ password: z.string().optional().describe("Ignored in this environment."),
1037
+ author: z.string().optional().describe("Replace all remaining markup authorship with this name."),
1038
+ export_pdf: z.boolean().optional().describe("Ignored in this environment.")
1039
+ }
1040
+ },
1041
+ async ({
1042
+ file_path,
1043
+ output_path,
1044
+ sanitize_mode,
1045
+ accept_all,
1046
+ protection_mode,
1047
+ author,
1048
+ export_pdf
1049
+ }) => {
1050
+ try {
1051
+ let outPath = output_path;
1052
+ if (!outPath) {
1053
+ const ext = extname(file_path);
1054
+ const base = basename2(file_path, ext);
1055
+ const dir = dirname(file_path);
1056
+ outPath = resolve2(dir, `${base}_final${ext}`);
1003
1057
  }
1004
- throw new Error(`Unknown tool: ${name}`);
1005
- } catch (error) {
1058
+ const buf = readFileBytesOrThrow(file_path);
1059
+ const doc = await DocumentObject2.load(buf);
1060
+ const result = await finalize_document(doc, {
1061
+ filename: basename2(file_path),
1062
+ sanitize_mode: sanitize_mode || "full",
1063
+ accept_all,
1064
+ protection_mode,
1065
+ author,
1066
+ export_pdf
1067
+ });
1068
+ fs.writeFileSync(outPath, result.outBuffer);
1006
1069
  return {
1007
1070
  content: [
1008
1071
  {
1009
1072
  type: "text",
1010
- text: `Error executing tool ${name}: ${error.message}`
1073
+ text: `Saved to: ${outPath}
1074
+
1075
+ ${result.reportText}`
1011
1076
  }
1012
- ],
1013
- isError: true
1077
+ ]
1014
1078
  };
1079
+ } catch (e) {
1080
+ return {
1081
+ isError: true,
1082
+ content: [{ type: "text", text: `Error: ${e.message}` }]
1083
+ };
1084
+ }
1085
+ }
1086
+ );
1087
+ server.registerTool(
1088
+ "login_to_adeu_cloud",
1089
+ { description: "Logs the user into the Adeu Cloud backend." },
1090
+ async () => {
1091
+ try {
1092
+ return await login_to_adeu_cloud();
1093
+ } catch (e) {
1094
+ return { isError: true, content: [{ type: "text", text: e.message }] };
1095
+ }
1096
+ }
1097
+ );
1098
+ server.registerTool(
1099
+ "logout_of_adeu_cloud",
1100
+ { description: "Logs out of the Adeu Cloud backend." },
1101
+ async () => {
1102
+ try {
1103
+ return await logout_of_adeu_cloud();
1104
+ } catch (e) {
1105
+ return { isError: true, content: [{ type: "text", text: e.message }] };
1106
+ }
1107
+ }
1108
+ );
1109
+ server.registerTool(
1110
+ "create_email_draft",
1111
+ {
1112
+ description: "Creates an email draft in the user's native draft box.",
1113
+ inputSchema: {
1114
+ body_markdown: z.string(),
1115
+ reply_to_email_id: z.string().optional(),
1116
+ subject: z.string().optional(),
1117
+ to_recipients: z.array(z.string()).optional(),
1118
+ attachment_paths: z.array(z.string()).optional(),
1119
+ mailbox_address: z.string().optional().describe(
1120
+ "Optional target mailbox email address to create the draft in."
1121
+ )
1122
+ }
1123
+ },
1124
+ async (args) => {
1125
+ try {
1126
+ return await create_email_draft(args);
1127
+ } catch (e) {
1128
+ return { isError: true, content: [{ type: "text", text: e.message }] };
1129
+ }
1130
+ }
1131
+ );
1132
+ server.registerTool(
1133
+ "list_available_mailboxes",
1134
+ {
1135
+ description: "Lists all personal and shared delegated mailboxes configured for the authenticated profile. Use this to discover valid email addresses to scope search and draft operations.",
1136
+ inputSchema: {}
1137
+ },
1138
+ async () => {
1139
+ try {
1140
+ return await list_available_mailboxes();
1141
+ } catch (e) {
1142
+ return { isError: true, content: [{ type: "text", text: e.message }] };
1015
1143
  }
1016
1144
  }
1017
1145
  );