@adeu/mcp-server 1.8.0 → 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/assets/adeu.svg +3 -0
- package/dist/assets/logo.png +0 -0
- package/dist/assets/marked.min.js +69 -0
- package/dist/index.js +523 -395
- package/dist/index.js.map +1 -1
- package/dist/templates/email_ui.html +667 -0
- package/dist/templates/markdown_ui.html +745 -0
- package/package.json +4 -2
- package/src/assets/adeu.svg +3 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/marked.min.js +69 -0
- package/src/index.ts +510 -407
- package/src/mcp.cloud.test.ts +13 -0
- package/src/response-builders.ts +111 -50
- package/src/templates/email_ui.html +667 -0
- package/src/templates/markdown_ui.html +745 -0
- package/src/tools/email.ts +61 -2
- package/tsup.config.ts +35 -11
package/src/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
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 {
|
|
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
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
server
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
});
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
);
|
|
282
280
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
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 (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
383
|
+
const buf = readFileBytesOrThrow(docx_path);
|
|
384
|
+
const doc = await DocumentObject.load(buf);
|
|
385
|
+
const engine = new RedlineEngine(doc);
|
|
382
386
|
|
|
383
|
-
|
|
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
|
-
|
|
391
|
-
const doc = await DocumentObject.load(buf);
|
|
392
|
-
const engine = new RedlineEngine(doc);
|
|
389
|
+
const outBuf = await doc.save();
|
|
393
390
|
|
|
394
|
-
|
|
395
|
-
engine.accept_all_revisions();
|
|
391
|
+
fs.writeFileSync(outPath, outBuf);
|
|
396
392
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
};
|
|
431
|
-
}
|
|
431
|
+
const origText = await extractTextFromBuffer(origBuf, compare_clean);
|
|
432
|
+
const modText = await extractTextFromBuffer(modBuf, compare_clean);
|
|
432
433
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
434
|
+
const diff = create_word_patch_diff(
|
|
435
|
+
origText,
|
|
436
|
+
modText,
|
|
437
|
+
basename(original_path),
|
|
438
|
+
basename(modified_path),
|
|
439
|
+
);
|
|
436
440
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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: `
|
|
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();
|