@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/src/index.ts CHANGED
@@ -1,11 +1,15 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1
+ // FILE: node/packages/mcp-server/src/index.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { readFileSync, existsSync } from "node:fs";
5
+ import { basename, resolve, extname, dirname, join } from "node:path";
6
+ import { z } from "zod";
3
7
  import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- } from "@modelcontextprotocol/sdk/types.js";
7
- import { readFileSync } from "node:fs";
8
- import { basename, resolve, extname, dirname } from "node:path";
8
+ registerAppTool,
9
+ registerAppResource,
10
+ RESOURCE_MIME_TYPE,
11
+ } from "@modelcontextprotocol/ext-apps/server";
12
+ import fs from "node:fs";
9
13
  import {
10
14
  identifyEngine,
11
15
  extractTextFromBuffer,
@@ -15,13 +19,21 @@ import {
15
19
  create_word_patch_diff,
16
20
  finalize_document,
17
21
  } from "@adeu/core";
22
+
18
23
  import {
19
24
  build_paginated_response,
20
25
  build_outline_response,
21
26
  build_appendix_response,
22
27
  } from "./response-builders.js";
28
+
23
29
  import { login_to_adeu_cloud, logout_of_adeu_cloud } from "./tools/auth.js";
24
- import { search_and_fetch_emails, create_email_draft } from "./tools/email.js";
30
+ import {
31
+ search_and_fetch_emails,
32
+ create_email_draft,
33
+ list_available_mailboxes,
34
+ } from "./tools/email.js";
35
+ import { MARKDOWN_UI_URI, EMAIL_UI_URI } from "./shared.js";
36
+
25
37
  function readFileBytesOrThrow(filePath: string): Buffer {
26
38
  try {
27
39
  return readFileSync(filePath);
@@ -32,7 +44,23 @@ function readFileBytesOrThrow(filePath: string): Buffer {
32
44
  throw err;
33
45
  }
34
46
  }
35
- // --- Tool Description Constants (Parity with Python) ---
47
+
48
+ // --- Asset Loaders for UI ---
49
+ const DIST_DIR = import.meta.dirname;
50
+
51
+ function getAssetContent(
52
+ folder: "templates" | "assets",
53
+ filename: string,
54
+ fallbackMessage: string,
55
+ ): string {
56
+ const filePath = join(DIST_DIR, folder, filename);
57
+ if (existsSync(filePath)) {
58
+ return readFileSync(filePath, "utf-8");
59
+ }
60
+ return fallbackMessage;
61
+ }
62
+
63
+ // --- Tool Description Constants ---
36
64
  const READ_DOCX_COMMON_DESC =
37
65
  "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";
38
66
  const READ_DOCX_TAIL =
@@ -47,451 +75,526 @@ const DIFF_DOCX_DESC =
47
75
  "Compares two DOCX files and returns a unified diff of their text content. Useful for analyzing differences between versions before editing.";
48
76
 
49
77
  // --- Server Setup ---
50
- const server = new Server(
51
- {
52
- name: "adeu-redlining-service",
53
- version: "1.0.0",
54
- },
55
- {
56
- capabilities: {
57
- tools: {},
58
- },
78
+ const server = new McpServer({
79
+ name: "adeu-redlining-service",
80
+ version: "1.0.0",
81
+ });
82
+
83
+ // Common CSP allowing Google Fonts used by Adeu UI templates
84
+ const UI_CSP = {
85
+ connectDomains: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
86
+ resourceDomains: [
87
+ "https://fonts.googleapis.com",
88
+ "https://fonts.gstatic.com",
89
+ ],
90
+ };
91
+
92
+ // ==========================================
93
+ // 1. UI RESOURCES
94
+ // ==========================================
95
+
96
+ registerAppResource(
97
+ server,
98
+ MARKDOWN_UI_URI,
99
+ MARKDOWN_UI_URI,
100
+ { mimeType: RESOURCE_MIME_TYPE, description: "Adeu Markdown Viewer UI" },
101
+ async () => {
102
+ let html = getAssetContent(
103
+ "templates",
104
+ "markdown_ui.html",
105
+ "<html><body>UI Template Not Found</body></html>",
106
+ );
107
+ const markedJs = getAssetContent(
108
+ "assets",
109
+ "marked.min.js",
110
+ "window.__MARKED_ERROR = 'marked.min.js not found';",
111
+ );
112
+ const svg = getAssetContent("assets", "adeu.svg", "");
113
+
114
+ html = html
115
+ .replace("[[marked_js_code | safe]]", markedJs)
116
+ .replace("[[ adeu_svg_code ]]", svg);
117
+
118
+ return {
119
+ contents: [
120
+ {
121
+ uri: MARKDOWN_UI_URI,
122
+ mimeType: RESOURCE_MIME_TYPE,
123
+ text: html,
124
+ _meta: { ui: { csp: UI_CSP } },
125
+ },
126
+ ],
127
+ };
59
128
  },
60
129
  );
61
130
 
62
- // --- Tool Registration ---
63
- server.setRequestHandler(ListToolsRequestSchema, async () => {
64
- return {
65
- tools: [
66
- {
67
- name: "read_docx",
68
- description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,
69
- inputSchema: {
70
- type: "object",
71
- properties: {
72
- file_path: {
73
- type: "string",
74
- description: "Absolute path to the DOCX file.",
75
- },
76
- clean_view: {
77
- type: "boolean",
78
- description:
79
- "If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text.",
80
- default: false,
81
- },
82
- mode: {
83
- type: "string",
84
- enum: ["full", "outline", "appendix"],
85
- description:
86
- "'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms.",
87
- default: "full",
88
- },
89
- page: {
90
- type: "number",
91
- description:
92
- "Page number (1-indexed) for mode='full'. Defaults to 1.",
93
- default: 1,
94
- },
95
- outline_max_level: {
96
- type: "number",
97
- description: "For mode='outline' only: cap on heading depth.",
98
- default: 2,
99
- },
100
- outline_verbose: {
101
- type: "boolean",
102
- description: "For mode='outline' only: includes metadata.",
103
- default: false,
104
- },
105
- },
106
- required: ["file_path"],
107
- },
108
- },
109
- {
110
- name: "process_document_batch",
111
- description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,
112
- inputSchema: {
113
- type: "object",
114
- properties: {
115
- original_docx_path: {
116
- type: "string",
117
- description: "Absolute path to the source file.",
118
- },
119
- author_name: {
120
- type: "string",
121
- description:
122
- "Name to appear in Track Changes (e.g., 'Reviewer AI').",
123
- },
124
- changes: {
125
- type: "array",
126
- description:
127
- "List of changes to apply. Each change must specify 'type'.",
128
- items: { type: "object" },
129
- },
130
- output_path: {
131
- type: "string",
132
- description: "Optional output path.",
133
- },
134
- },
135
- required: ["original_docx_path", "author_name", "changes"],
136
- },
137
- },
138
- {
139
- name: "accept_all_changes",
140
- description:
141
- "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.",
142
- inputSchema: {
143
- type: "object",
144
- properties: {
145
- docx_path: {
146
- type: "string",
147
- description: "Absolute path to the DOCX file.",
148
- },
149
- output_path: {
150
- type: "string",
151
- description: "Optional output path.",
152
- },
153
- },
154
- required: ["docx_path"],
155
- },
156
- },
157
- {
158
- name: "diff_docx_files",
159
- description: DIFF_DOCX_DESC,
160
- inputSchema: {
161
- type: "object",
162
- properties: {
163
- original_path: {
164
- type: "string",
165
- description: "Absolute path to the baseline DOCX file.",
166
- },
167
- modified_path: {
168
- type: "string",
169
- description: "Absolute path to the modified DOCX file.",
170
- },
171
- compare_clean: {
172
- type: "boolean",
173
- description:
174
- "If True, compares 'Accepted' state. If False, compares raw text.",
175
- default: true,
176
- },
177
- },
178
- required: ["original_path", "modified_path"],
131
+ registerAppResource(
132
+ server,
133
+ EMAIL_UI_URI,
134
+ EMAIL_UI_URI,
135
+ { mimeType: RESOURCE_MIME_TYPE, description: "Adeu Email Viewer UI" },
136
+ async () => {
137
+ let html = getAssetContent(
138
+ "templates",
139
+ "email_ui.html",
140
+ "<html><body>UI Template Not Found</body></html>",
141
+ );
142
+ const svg = getAssetContent("assets", "adeu.svg", "");
143
+
144
+ html = html.replace("[[ adeu_svg_code ]]", svg);
145
+
146
+ return {
147
+ contents: [
148
+ {
149
+ uri: EMAIL_UI_URI,
150
+ mimeType: RESOURCE_MIME_TYPE,
151
+ text: html,
152
+ _meta: { ui: { csp: UI_CSP } },
179
153
  },
180
- },
181
- {
182
- name: "finalize_document",
183
- description:
184
- "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.",
185
- inputSchema: {
186
- type: "object",
187
- properties: {
188
- file_path: {
189
- type: "string",
190
- description: "Absolute path to the DOCX file.",
191
- },
192
- output_path: {
193
- type: "string",
194
- description: "Optional output path.",
195
- },
196
- sanitize_mode: {
197
- type: "string",
198
- enum: ["full", "keep-markup"],
199
- description:
200
- "full removes all markup, keep-markup redacts metadata but keeps comments/redlines.",
201
- },
202
- accept_all: {
203
- type: "boolean",
204
- description:
205
- "If true, auto-accepts all unresolved track changes before finalizing.",
206
- },
207
- protection_mode: {
208
- type: "string",
209
- enum: ["read_only", "encrypt"],
210
- description:
211
- "Native OOXML document locking. encrypt falls back to read_only in this environment.",
212
- },
213
- password: {
214
- type: "string",
215
- description: "Ignored in this environment.",
216
- },
217
- author: {
218
- type: "string",
219
- description:
220
- "Replace all remaining markup authorship with this name.",
221
- },
222
- export_pdf: {
223
- type: "boolean",
224
- description: "Ignored in this environment.",
225
- },
226
- },
227
- required: ["file_path"],
228
- },
229
- },
230
- {
231
- name: "login_to_adeu_cloud",
232
- description:
233
- "Logs the user into the Adeu Cloud backend. Securely opens a browser window for authentication.",
234
- inputSchema: { type: "object", properties: {} },
235
- },
236
- {
237
- name: "logout_of_adeu_cloud",
238
- description:
239
- "Logs out of the Adeu Cloud backend by clearing the local API key.",
240
- inputSchema: { type: "object", properties: {} },
241
- },
242
- {
243
- name: "search_and_fetch_emails",
244
- description:
245
- "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.",
246
- inputSchema: {
247
- type: "object",
248
- properties: {
249
- sender: { type: "string" },
250
- subject: { type: "string" },
251
- has_attachments: { type: "boolean" },
252
- attachment_name: { type: "string" },
253
- is_unread: { type: "boolean" },
254
- days_ago: { type: "number" },
255
- folder: { type: "string", enum: ["inbox", "sent", "all"] },
256
- limit: { type: "number", default: 10 },
257
- offset: { type: "number", default: 0 },
258
- email_id: { type: "string" },
259
- working_directory: { type: "string" },
154
+ ],
155
+ };
156
+ },
157
+ );
158
+
159
+ // ==========================================
160
+ // 2. UI-ENABLED TOOLS
161
+ // ==========================================
162
+
163
+ registerAppTool(
164
+ server,
165
+ "read_docx",
166
+ {
167
+ title: "Read DOCX",
168
+ description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,
169
+ inputSchema: z.object({
170
+ file_path: z.string().describe("Absolute path to the DOCX file."),
171
+ clean_view: z
172
+ .boolean()
173
+ .default(false)
174
+ .describe(
175
+ "If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text.",
176
+ ),
177
+ mode: z
178
+ .enum(["full", "outline", "appendix"])
179
+ .default("full")
180
+ .describe(
181
+ "'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms.",
182
+ ),
183
+ page: z
184
+ .number()
185
+ .default(1)
186
+ .describe("Page number (1-indexed) for mode='full'. Defaults to 1."),
187
+ outline_max_level: z
188
+ .number()
189
+ .default(2)
190
+ .describe("For mode='outline' only: cap on heading depth."),
191
+ outline_verbose: z
192
+ .boolean()
193
+ .default(false)
194
+ .describe("For mode='outline' only: includes metadata."),
195
+ }),
196
+ _meta: { ui: { resourceUri: MARKDOWN_UI_URI } },
197
+ },
198
+ async ({
199
+ file_path,
200
+ clean_view,
201
+ mode,
202
+ page,
203
+ outline_max_level,
204
+ outline_verbose,
205
+ }) => {
206
+ try {
207
+ const buf = readFileBytesOrThrow(file_path);
208
+ const text = await extractTextFromBuffer(buf, clean_view);
209
+
210
+ if (mode === "outline") {
211
+ const doc = await DocumentObject.load(buf);
212
+ return build_outline_response(
213
+ doc,
214
+ text,
215
+ file_path,
216
+ outline_max_level,
217
+ outline_verbose,
218
+ ) as any;
219
+ }
220
+ if (mode === "appendix") {
221
+ return build_appendix_response(text, page, file_path) as any;
222
+ }
223
+ return build_paginated_response(text, page, file_path) as any;
224
+ } catch (e: any) {
225
+ return {
226
+ isError: true,
227
+ content: [
228
+ {
229
+ type: "text",
230
+ text: `Error executing tool read_docx: ${e.message}`,
260
231
  },
261
- },
262
- },
263
- {
264
- name: "create_email_draft",
265
- description:
266
- "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.",
267
- inputSchema: {
268
- type: "object",
269
- properties: {
270
- body_markdown: { type: "string" },
271
- reply_to_email_id: { type: "string" },
272
- subject: { type: "string" },
273
- to_recipients: { type: "array", items: { type: "string" } },
274
- attachment_paths: { type: "array", items: { type: "string" } },
232
+ ],
233
+ };
234
+ }
235
+ },
236
+ );
237
+
238
+ registerAppTool(
239
+ server,
240
+ "search_and_fetch_emails",
241
+ {
242
+ title: "Search & Fetch Emails",
243
+ description:
244
+ "Searches the user's live email inbox. Returns previews. Call again with `email_id` to fetch the full body.",
245
+ inputSchema: z.object({
246
+ sender: z.string().optional(),
247
+ subject: z.string().optional(),
248
+ has_attachments: z.boolean().optional(),
249
+ attachment_name: z.string().optional(),
250
+ is_unread: z.boolean().optional(),
251
+ days_ago: z.number().optional(),
252
+ folder: z.enum(["inbox", "sent", "all"]).optional(),
253
+ limit: z.number().default(10),
254
+ offset: z.number().default(0),
255
+ email_id: z.string().optional(),
256
+ working_directory: z.string().optional(),
257
+ mailbox_address: z
258
+ .string()
259
+ .optional()
260
+ .describe("Optional target mailbox email address to search within."),
261
+ }),
262
+ _meta: { ui: { resourceUri: EMAIL_UI_URI } },
263
+ },
264
+ async (args) => {
265
+ try {
266
+ return (await search_and_fetch_emails(args)) as any;
267
+ } catch (e: any) {
268
+ return {
269
+ isError: true,
270
+ content: [
271
+ {
272
+ type: "text",
273
+ text: `Error executing tool search_and_fetch_emails: ${e.message}`,
275
274
  },
276
- required: ["body_markdown"],
277
- },
278
- },
279
- ],
280
- };
281
- });
275
+ ],
276
+ };
277
+ }
278
+ },
279
+ );
282
280
 
283
- // --- Tool Execution ---
284
- server.setRequestHandler(
285
- CallToolRequestSchema,
286
- async (request): Promise<any> => {
287
- const { name, arguments: args } = request.params;
281
+ // ==========================================
282
+ // 3. HEADLESS TOOLS (No UI)
283
+ // ==========================================
288
284
 
285
+ server.registerTool(
286
+ "process_document_batch",
287
+ {
288
+ description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,
289
+ inputSchema: {
290
+ original_docx_path: z
291
+ .string()
292
+ .describe("Absolute path to the source file."),
293
+ author_name: z
294
+ .string()
295
+ .describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
296
+ changes: z
297
+ .array(z.any())
298
+ .describe("List of changes to apply. Each change must specify 'type'."),
299
+ output_path: z.string().optional().describe("Optional output path."),
300
+ },
301
+ },
302
+ async ({ original_docx_path, author_name, changes, output_path }) => {
289
303
  try {
290
- if (name === "read_docx") {
291
- const filePath = args?.file_path as string;
292
- const cleanView = (args?.clean_view as boolean) ?? false;
293
- const mode = (args?.mode as string) ?? "full";
294
- const page = (args?.page as number) ?? 1;
295
- const outline_max_level = (args?.outline_max_level as number) ?? 2;
296
- const outline_verbose = (args?.outline_verbose as boolean) ?? false;
297
-
298
- const buf = readFileBytesOrThrow(filePath);
299
- const text = await extractTextFromBuffer(buf, cleanView);
300
-
301
- if (mode === "outline") {
302
- const doc = await DocumentObject.load(buf);
303
- return build_outline_response(
304
- doc,
305
- text,
306
- filePath,
307
- outline_max_level,
308
- outline_verbose,
309
- );
310
- }
311
- if (mode === "appendix") {
312
- return build_appendix_response(text, page, filePath);
313
- }
314
- return build_paginated_response(text, page, filePath);
304
+ if (!author_name || !author_name.trim())
305
+ return {
306
+ content: [
307
+ { type: "text", text: "Error: author_name cannot be empty." },
308
+ ],
309
+ };
310
+ if (!changes || changes.length === 0)
311
+ return {
312
+ content: [{ type: "text", text: "Error: No changes provided." }],
313
+ };
314
+
315
+ let outPath = output_path;
316
+ if (!outPath) {
317
+ const ext = extname(original_docx_path);
318
+ const base = basename(original_docx_path, ext);
319
+ const dir = dirname(original_docx_path);
320
+ outPath = resolve(dir, `${base}_processed${ext}`);
315
321
  }
316
- if (name === "process_document_batch") {
317
- const origPath = args?.original_docx_path as string;
318
- const authorName = args?.author_name as string;
319
- const changes = args?.changes as any[];
320
- let outPath = args?.output_path as string;
321
322
 
322
- if (!authorName || !authorName.trim()) {
323
+ const buf = readFileBytesOrThrow(original_docx_path);
324
+ const doc = await DocumentObject.load(buf);
325
+ const engine = new RedlineEngine(doc, author_name);
326
+
327
+ let stats;
328
+ try {
329
+ stats = engine.process_batch(changes);
330
+ } catch (e: any) {
331
+ if (e instanceof BatchValidationError) {
323
332
  return {
333
+ isError: true,
324
334
  content: [
325
- { type: "text", text: "Error: author_name cannot be empty." },
335
+ {
336
+ type: "text",
337
+ text: `Batch rejected. Some edits failed validation:\n\n${e.errors.join("\n\n")}`,
338
+ },
326
339
  ],
327
340
  };
328
341
  }
342
+ throw e;
343
+ }
329
344
 
330
- if (!changes || changes.length === 0) {
331
- return {
332
- content: [{ type: "text", text: "Error: No changes provided." }],
333
- };
334
- }
335
- if (!outPath) {
336
- const ext = extname(origPath);
337
- const base = basename(origPath, ext);
338
- const dir = dirname(origPath);
339
- outPath = resolve(dir, `${base}_processed${ext}`);
340
- }
341
-
342
- const buf = readFileBytesOrThrow(origPath);
343
- const doc = await DocumentObject.load(buf);
344
- const engine = new RedlineEngine(doc, authorName);
345
-
346
- let stats;
347
- try {
348
- stats = engine.process_batch(changes);
349
- } catch (e) {
350
- if (e instanceof BatchValidationError) {
351
- return {
352
- content: [
353
- {
354
- type: "text",
355
- text: `Batch rejected. Some edits failed validation:\n\n${(e as BatchValidationError).errors.join("\n\n")}`,
356
- },
357
- ],
358
- isError: true,
359
- };
360
- }
361
- throw e;
362
- }
345
+ const outBuf = await doc.save();
363
346
 
364
- const outBuf = await doc.save();
365
- // Using dynamic import of fs/promises or just sync write
366
- const fs = await import("node:fs");
367
- fs.writeFileSync(outPath, outBuf);
347
+ fs.writeFileSync(outPath, outBuf);
368
348
 
369
- let res = `Batch complete. Saved to: ${outPath}\nActions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.\nEdits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.`;
370
- if (stats.skipped_details?.length > 0) {
371
- res += `\n\nSkipped Details:\n${stats.skipped_details.join("\n")}`;
372
- }
349
+ let res = `Batch complete. Saved to: ${outPath}\nActions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.\nEdits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.`;
350
+ if (stats.skipped_details?.length > 0) {
351
+ res += `\n\nSkipped Details:\n${stats.skipped_details.join("\n")}`;
352
+ }
353
+ return { content: [{ type: "text", text: res }] };
354
+ } catch (e: any) {
355
+ return {
356
+ isError: true,
357
+ content: [{ type: "text", text: `Error: ${e.message}` }],
358
+ };
359
+ }
360
+ },
361
+ );
373
362
 
374
- return {
375
- content: [{ type: "text", text: res }],
376
- };
363
+ server.registerTool(
364
+ "accept_all_changes",
365
+ {
366
+ description:
367
+ "Accepts all tracked changes and removes all comments in a single operation.",
368
+ inputSchema: {
369
+ docx_path: z.string().describe("Absolute path to the DOCX file."),
370
+ output_path: z.string().optional().describe("Optional output path."),
371
+ },
372
+ },
373
+ async ({ docx_path, output_path }) => {
374
+ try {
375
+ let outPath = output_path;
376
+ if (!outPath) {
377
+ const ext = extname(docx_path);
378
+ const base = basename(docx_path, ext);
379
+ const dir = dirname(docx_path);
380
+ outPath = resolve(dir, `${base}_clean${ext}`);
377
381
  }
378
382
 
379
- if (name === "accept_all_changes") {
380
- const docxPath = args?.docx_path as string;
381
- let outPath = args?.output_path as string;
383
+ const buf = readFileBytesOrThrow(docx_path);
384
+ const doc = await DocumentObject.load(buf);
385
+ const engine = new RedlineEngine(doc);
382
386
 
383
- if (!outPath) {
384
- const ext = extname(docxPath);
385
- const base = basename(docxPath, ext);
386
- const dir = dirname(docxPath);
387
- outPath = resolve(dir, `${base}_clean${ext}`);
388
- }
387
+ engine.accept_all_revisions();
389
388
 
390
- const buf = readFileBytesOrThrow(docxPath);
391
- const doc = await DocumentObject.load(buf);
392
- const engine = new RedlineEngine(doc);
389
+ const outBuf = await doc.save();
393
390
 
394
- // We implement the public facing accept_all wrapper from python
395
- engine.accept_all_revisions();
391
+ fs.writeFileSync(outPath, outBuf);
396
392
 
397
- const outBuf = await doc.save();
398
- const fs = await import("node:fs");
399
- fs.writeFileSync(outPath, outBuf);
393
+ return {
394
+ content: [
395
+ { type: "text", text: `Accepted all changes. Saved to: ${outPath}` },
396
+ ],
397
+ };
398
+ } catch (e: any) {
399
+ return {
400
+ isError: true,
401
+ content: [{ type: "text", text: `Error: ${e.message}` }],
402
+ };
403
+ }
404
+ },
405
+ );
400
406
 
401
- return {
402
- content: [
403
- {
404
- type: "text",
405
- text: `Accepted all changes. Saved to: ${outPath}`,
406
- },
407
- ],
408
- };
409
- }
410
- if (name === "diff_docx_files") {
411
- const origPath = args?.original_path as string;
412
- const modPath = args?.modified_path as string;
413
- const compareClean = (args?.compare_clean as boolean) ?? true;
414
- const origBuf = readFileBytesOrThrow(origPath);
415
- const modBuf = readFileBytesOrThrow(modPath);
416
-
417
- // Pass compareClean flag into extraction
418
- const origText = await extractTextFromBuffer(origBuf, compareClean);
419
- const modText = await extractTextFromBuffer(modBuf, compareClean);
420
-
421
- const diff = create_word_patch_diff(
422
- origText,
423
- modText,
424
- basename(origPath),
425
- basename(modPath),
426
- );
407
+ server.registerTool(
408
+ "diff_docx_files",
409
+ {
410
+ description: DIFF_DOCX_DESC,
411
+ inputSchema: {
412
+ original_path: z
413
+ .string()
414
+ .describe("Absolute path to the baseline DOCX file."),
415
+ modified_path: z
416
+ .string()
417
+ .describe("Absolute path to the modified DOCX file."),
418
+ compare_clean: z
419
+ .boolean()
420
+ .default(true)
421
+ .describe(
422
+ "If True, compares 'Accepted' state. If False, compares raw text.",
423
+ ),
424
+ },
425
+ },
426
+ async ({ original_path, modified_path, compare_clean }) => {
427
+ try {
428
+ const origBuf = readFileBytesOrThrow(original_path);
429
+ const modBuf = readFileBytesOrThrow(modified_path);
427
430
 
428
- return {
429
- content: [{ type: "text", text: diff || "No differences found." }],
430
- };
431
- }
431
+ const origText = await extractTextFromBuffer(origBuf, compare_clean);
432
+ const modText = await extractTextFromBuffer(modBuf, compare_clean);
432
433
 
433
- if (name === "finalize_document") {
434
- const filePath = args?.file_path as string;
435
- let outPath = args?.output_path as string;
434
+ const diff = create_word_patch_diff(
435
+ origText,
436
+ modText,
437
+ basename(original_path),
438
+ basename(modified_path),
439
+ );
436
440
 
437
- if (!outPath) {
438
- const ext = extname(filePath);
439
- const base = basename(filePath, ext);
440
- const dir = dirname(filePath);
441
- outPath = resolve(dir, `${base}_final${ext}`);
442
- }
441
+ return {
442
+ content: [{ type: "text", text: diff || "No differences found." }],
443
+ };
444
+ } catch (e: any) {
445
+ return {
446
+ isError: true,
447
+ content: [{ type: "text", text: `Error: ${e.message}` }],
448
+ };
449
+ }
450
+ },
451
+ );
443
452
 
444
- const buf = readFileBytesOrThrow(filePath);
445
- const doc = await DocumentObject.load(buf);
453
+ server.registerTool(
454
+ "finalize_document",
455
+ {
456
+ description:
457
+ "Prepares a document for external distribution or e-signature.",
458
+ inputSchema: {
459
+ file_path: z.string().describe("Absolute path to the DOCX file."),
460
+ output_path: z.string().optional().describe("Optional output path."),
461
+ sanitize_mode: z
462
+ .enum(["full", "keep-markup"])
463
+ .optional()
464
+ .describe("full removes all markup, keep-markup redacts metadata."),
465
+ accept_all: z
466
+ .boolean()
467
+ .optional()
468
+ .describe(
469
+ "If true, auto-accepts all unresolved track changes before finalizing.",
470
+ ),
471
+ protection_mode: z
472
+ .enum(["read_only", "encrypt"])
473
+ .optional()
474
+ .describe("Native OOXML document locking."),
475
+ password: z.string().optional().describe("Ignored in this environment."),
476
+ author: z
477
+ .string()
478
+ .optional()
479
+ .describe("Replace all remaining markup authorship with this name."),
480
+ export_pdf: z
481
+ .boolean()
482
+ .optional()
483
+ .describe("Ignored in this environment."),
484
+ },
485
+ },
486
+ async ({
487
+ file_path,
488
+ output_path,
489
+ sanitize_mode,
490
+ accept_all,
491
+ protection_mode,
492
+ author,
493
+ export_pdf,
494
+ }) => {
495
+ try {
496
+ let outPath = output_path;
497
+ if (!outPath) {
498
+ const ext = extname(file_path);
499
+ const base = basename(file_path, ext);
500
+ const dir = dirname(file_path);
501
+ outPath = resolve(dir, `${base}_final${ext}`);
502
+ }
446
503
 
447
- const result = await finalize_document(doc, {
448
- filename: basename(filePath),
449
- sanitize_mode: (args?.sanitize_mode as any) || "full",
450
- accept_all: args?.accept_all as boolean,
451
- protection_mode: args?.protection_mode as any,
452
- author: args?.author as string,
453
- export_pdf: args?.export_pdf as boolean,
454
- });
504
+ const buf = readFileBytesOrThrow(file_path);
505
+ const doc = await DocumentObject.load(buf);
455
506
 
456
- const fs = await import("node:fs");
457
- fs.writeFileSync(outPath, result.outBuffer!);
507
+ const result = await finalize_document(doc, {
508
+ filename: basename(file_path),
509
+ sanitize_mode: (sanitize_mode as any) || "full",
510
+ accept_all: accept_all as boolean,
511
+ protection_mode: protection_mode as any,
512
+ author: author as string,
513
+ export_pdf: export_pdf as boolean,
514
+ });
515
+
516
+ fs.writeFileSync(outPath, result.outBuffer!);
458
517
 
459
- return {
460
- content: [
461
- {
462
- type: "text",
463
- text: `Saved to: ${outPath}\n\n${result.reportText}`,
464
- },
465
- ],
466
- };
467
- }
468
- if (name === "login_to_adeu_cloud") {
469
- return await login_to_adeu_cloud();
470
- }
471
- if (name === "logout_of_adeu_cloud") {
472
- return await logout_of_adeu_cloud();
473
- }
474
- if (name === "search_and_fetch_emails") {
475
- return await search_and_fetch_emails(args || {});
476
- }
477
- if (name === "create_email_draft") {
478
- return await create_email_draft(args || {});
479
- }
480
- throw new Error(`Unknown tool: ${name}`);
481
- } catch (error: any) {
482
518
  return {
483
519
  content: [
484
520
  {
485
521
  type: "text",
486
- text: `Error executing tool ${name}: ${error.message}`,
522
+ text: `Saved to: ${outPath}\n\n${result.reportText}`,
487
523
  },
488
524
  ],
525
+ };
526
+ } catch (e: any) {
527
+ return {
489
528
  isError: true,
529
+ content: [{ type: "text", text: `Error: ${e.message}` }],
490
530
  };
491
531
  }
492
532
  },
493
533
  );
534
+ server.registerTool(
535
+ "login_to_adeu_cloud",
536
+ { description: "Logs the user into the Adeu Cloud backend." },
537
+ async () => {
538
+ try {
539
+ return (await login_to_adeu_cloud()) as any;
540
+ } catch (e: any) {
541
+ return { isError: true, content: [{ type: "text", text: e.message }] };
542
+ }
543
+ },
544
+ );
494
545
 
546
+ server.registerTool(
547
+ "logout_of_adeu_cloud",
548
+ { description: "Logs out of the Adeu Cloud backend." },
549
+ async () => {
550
+ try {
551
+ return (await logout_of_adeu_cloud()) as any;
552
+ } catch (e: any) {
553
+ return { isError: true, content: [{ type: "text", text: e.message }] };
554
+ }
555
+ },
556
+ );
557
+ server.registerTool(
558
+ "create_email_draft",
559
+ {
560
+ description: "Creates an email draft in the user's native draft box.",
561
+ inputSchema: {
562
+ body_markdown: z.string(),
563
+ reply_to_email_id: z.string().optional(),
564
+ subject: z.string().optional(),
565
+ to_recipients: z.array(z.string()).optional(),
566
+ attachment_paths: z.array(z.string()).optional(),
567
+ mailbox_address: z
568
+ .string()
569
+ .optional()
570
+ .describe(
571
+ "Optional target mailbox email address to create the draft in.",
572
+ ),
573
+ },
574
+ },
575
+ async (args) => {
576
+ try {
577
+ return (await create_email_draft(args)) as any;
578
+ } catch (e: any) {
579
+ return { isError: true, content: [{ type: "text", text: e.message }] };
580
+ }
581
+ },
582
+ );
583
+ server.registerTool(
584
+ "list_available_mailboxes",
585
+ {
586
+ description:
587
+ "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.",
588
+ inputSchema: {},
589
+ },
590
+ async () => {
591
+ try {
592
+ return (await list_available_mailboxes()) as any;
593
+ } catch (e: any) {
594
+ return { isError: true, content: [{ type: "text", text: e.message }] };
595
+ }
596
+ },
597
+ );
495
598
  // --- Startup ---
496
599
  async function main() {
497
600
  const transport = new StdioServerTransport();