@adeu/mcp-server 1.6.9 → 1.7.3
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/README.md +42 -0
- package/dist/index.js +495 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/desktop-auth.ts +127 -0
- package/src/index.ts +67 -5
- package/src/mcp.cloud.test.ts +138 -0
- package/src/shared.ts +7 -0
- package/src/tools/auth.ts +49 -0
- package/src/tools/email.ts +313 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
# @adeu/mcp-server
|
|
3
|
+
|
|
4
|
+
[](https://github.com/dealfluence/adeu)
|
|
5
|
+
[](https://modelcontextprotocol.io/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**Native Microsoft Word Track Changes for AI Agents**
|
|
9
|
+
|
|
10
|
+
`@adeu/mcp-server` is a standalone Node.js [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that acts as a "Virtual DOM" for Microsoft Word documents. It provides AI agents (like Claude) with tools to safely read, edit, and sanitize `.docx` files without destroying underlying formatting or complex XML.
|
|
11
|
+
|
|
12
|
+
This package provides the exact same engine as the Python `adeu` CLI, but executes entirely via Node.js—making it ideal for environments where Python is unavailable or undesired.
|
|
13
|
+
|
|
14
|
+
## Usage with MCP Clients
|
|
15
|
+
|
|
16
|
+
You can add this server directly to your MCP client (like Claude Desktop, Cursor, or Windsurf) using `npx`.
|
|
17
|
+
|
|
18
|
+
Add the following to your MCP configuration file (`claude_desktop_config.json`):
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"adeu-node": {
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": ["-y", "@adeu/mcp-server"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Exposed Tools
|
|
32
|
+
|
|
33
|
+
Once connected, your AI agent will have access to the following tools:
|
|
34
|
+
|
|
35
|
+
- `read_docx`: Reads a DOCX file and returns LLM-friendly text with inline CriticMarkup (`{++inserted++}`, `{--deleted--}`) for Tracked Changes and Comments. Supports pagination, structural outlining, and semantic appendix extraction.
|
|
36
|
+
- `process_document_batch`: Applies a batch of search-and-replace text modifications, table edits, and comment replies to a document. Translates the LLM's edits into perfectly formatted native Word Track Changes.
|
|
37
|
+
- `accept_all_changes`: Accepts all tracked changes and removes all comments to produce a finalized clean document.
|
|
38
|
+
- `diff_docx_files`: Compares two DOCX files and returns a unified sub-word diff of their text content.
|
|
39
|
+
- `finalize_document`: Prepares a document for signature by applying native OOXML read-only locking and deep metadata sanitization.
|
|
40
|
+
|
|
41
|
+
## Documentation & Support
|
|
42
|
+
For full architectural details, prompt recommendations, and the project constitution, please visit the [main Adeu repository](https://github.com/dealfluence/adeu) or our [website](https://adeu.ai).
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import {
|
|
7
7
|
CallToolRequestSchema,
|
|
8
8
|
ListToolsRequestSchema
|
|
9
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import { readFileSync } from "fs";
|
|
10
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
11
11
|
import { basename as basename2, resolve as resolve2, extname, dirname } from "path";
|
|
12
12
|
import {
|
|
13
13
|
identifyEngine,
|
|
@@ -160,10 +160,444 @@ ${ui_markdown}`;
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// src/desktop-auth.ts
|
|
164
|
+
import { createServer } from "http";
|
|
165
|
+
import { exec } from "child_process";
|
|
166
|
+
import { homedir, platform } from "os";
|
|
167
|
+
import { join } from "path";
|
|
168
|
+
import {
|
|
169
|
+
writeFileSync,
|
|
170
|
+
readFileSync,
|
|
171
|
+
mkdirSync,
|
|
172
|
+
existsSync,
|
|
173
|
+
rmSync,
|
|
174
|
+
chmodSync
|
|
175
|
+
} from "fs";
|
|
176
|
+
|
|
177
|
+
// src/shared.ts
|
|
178
|
+
var FRONTEND_URL = process.env.ADEU_FRONTEND_URL || "https://app.adeu.ai";
|
|
179
|
+
var BACKEND_URL = process.env.ADEU_BACKEND_URL || "https://app.adeu.ai";
|
|
180
|
+
|
|
181
|
+
// src/desktop-auth.ts
|
|
182
|
+
var ADEU_DIR = join(homedir(), ".adeu");
|
|
183
|
+
var CRED_PATH = join(ADEU_DIR, "credentials.json");
|
|
184
|
+
function openBrowser(url) {
|
|
185
|
+
if (platform() === "darwin") exec(`open "${url}"`);
|
|
186
|
+
else if (platform() === "win32") exec(`start "" "${url}"`);
|
|
187
|
+
else exec(`xdg-open "${url}"`);
|
|
188
|
+
}
|
|
189
|
+
var DesktopAuthManager = class {
|
|
190
|
+
static getApiKey() {
|
|
191
|
+
if (!existsSync(CRED_PATH)) return null;
|
|
192
|
+
try {
|
|
193
|
+
const data = JSON.parse(readFileSync(CRED_PATH, "utf-8"));
|
|
194
|
+
return data.api_key || null;
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
static setApiKey(apiKey) {
|
|
200
|
+
if (!existsSync(ADEU_DIR)) {
|
|
201
|
+
mkdirSync(ADEU_DIR, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
writeFileSync(CRED_PATH, JSON.stringify({ api_key: apiKey }));
|
|
204
|
+
chmodSync(CRED_PATH, 384);
|
|
205
|
+
}
|
|
206
|
+
static clearApiKey() {
|
|
207
|
+
if (existsSync(CRED_PATH)) {
|
|
208
|
+
rmSync(CRED_PATH);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
static async authenticateInteractive() {
|
|
212
|
+
return new Promise((resolve3, reject) => {
|
|
213
|
+
let server2;
|
|
214
|
+
const timeout = setTimeout(
|
|
215
|
+
() => {
|
|
216
|
+
if (server2) server2.close();
|
|
217
|
+
reject(new Error("Authentication timed out after 5 minutes."));
|
|
218
|
+
},
|
|
219
|
+
5 * 60 * 1e3
|
|
220
|
+
);
|
|
221
|
+
server2 = createServer((req, res) => {
|
|
222
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
223
|
+
if (url.pathname === "/callback") {
|
|
224
|
+
const apiKey = url.searchParams.get("api_key");
|
|
225
|
+
res.writeHead(apiKey ? 200 : 400, { "Content-Type": "text/html" });
|
|
226
|
+
const title = apiKey ? "Authentication Successful!" : "Authentication Failed";
|
|
227
|
+
const text = apiKey ? "Your Adeu MCP server has been successfully authenticated. You can safely close this window and return to Claude." : "No API key received. Please try again.";
|
|
228
|
+
const color = apiKey ? "#107c10" : "#d83b01";
|
|
229
|
+
res.end(`
|
|
230
|
+
<!DOCTYPE html><html><head><title>${title}</title>
|
|
231
|
+
<style>body{font-family:sans-serif;text-align:center;padding:50px;background:#f3f2f1;}.container{background:white;padding:40px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1);max-width:500px;margin:0 auto;}h1{color:${color};}p{color:#605e5c;line-height:1.5;}</style>
|
|
232
|
+
</head><body><div class="container"><h1>${title}</h1><p>${text}</p>
|
|
233
|
+
<script>setTimeout(()=>window.close(), 3000);</script>
|
|
234
|
+
</div></body></html>
|
|
235
|
+
`);
|
|
236
|
+
clearTimeout(timeout);
|
|
237
|
+
setTimeout(() => server2.close(), 100);
|
|
238
|
+
if (apiKey) {
|
|
239
|
+
this.setApiKey(apiKey);
|
|
240
|
+
resolve3(apiKey);
|
|
241
|
+
} else {
|
|
242
|
+
reject(new Error("No API key received in callback."));
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
res.writeHead(404);
|
|
246
|
+
res.end();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
250
|
+
const address = server2.address();
|
|
251
|
+
if (address && typeof address !== "string") {
|
|
252
|
+
const authUrl = `${FRONTEND_URL}/login?desktop_port=${address.port}`;
|
|
253
|
+
openBrowser(authUrl);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
static async ensureAuthenticated() {
|
|
259
|
+
const key = this.getApiKey();
|
|
260
|
+
if (key) return key;
|
|
261
|
+
return this.authenticateInteractive();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
async function getCloudAuthToken() {
|
|
265
|
+
const key = DesktopAuthManager.getApiKey();
|
|
266
|
+
if (!key) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"Authentication Required: You are not logged in. Please call the `login_to_adeu_cloud` tool first to authenticate, then try this task again."
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return key;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/tools/auth.ts
|
|
275
|
+
async function login_to_adeu_cloud() {
|
|
276
|
+
try {
|
|
277
|
+
const apiKey = await DesktopAuthManager.ensureAuthenticated();
|
|
278
|
+
const res = await fetch(`${BACKEND_URL}/api/v1/auth/me`, {
|
|
279
|
+
headers: {
|
|
280
|
+
Authorization: `Bearer ${apiKey}`,
|
|
281
|
+
Accept: "application/json"
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
if (res.status === 401) {
|
|
285
|
+
DesktopAuthManager.clearApiKey();
|
|
286
|
+
throw new Error(
|
|
287
|
+
"Your previous session expired. The stale key has been cleared. Please call `login_to_adeu_cloud` ONE MORE TIME to log in fresh."
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
|
291
|
+
const data = await res.json();
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: "text",
|
|
296
|
+
text: `Login successful! Connected to Adeu Cloud as: ${data.email || "Unknown Email"}.`
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
};
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return { isError: true, content: [{ type: "text", text: err.message }] };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function logout_of_adeu_cloud() {
|
|
305
|
+
DesktopAuthManager.clearApiKey();
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: "Successfully logged out. The local API key has been removed."
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/tools/email.ts
|
|
317
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
318
|
+
import { join as join2 } from "path";
|
|
319
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
320
|
+
import { createHash } from "crypto";
|
|
321
|
+
var CACHE_FILE = join2(homedir2(), ".adeu", "mcp_id_cache.json");
|
|
322
|
+
var MAX_CACHE_SIZE = 1e3;
|
|
323
|
+
function loadIdCache() {
|
|
324
|
+
if (existsSync2(CACHE_FILE)) {
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(readFileSync2(CACHE_FILE, "utf-8"));
|
|
327
|
+
} catch {
|
|
328
|
+
return {};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
function saveIdCache(cache) {
|
|
334
|
+
try {
|
|
335
|
+
mkdirSync2(join2(homedir2(), ".adeu"), { recursive: true });
|
|
336
|
+
const keys = Object.keys(cache);
|
|
337
|
+
if (keys.length > MAX_CACHE_SIZE) {
|
|
338
|
+
const trimmed = {};
|
|
339
|
+
keys.slice(-MAX_CACHE_SIZE).forEach((k) => trimmed[k] = cache[k]);
|
|
340
|
+
cache = trimmed;
|
|
341
|
+
}
|
|
342
|
+
writeFileSync2(CACHE_FILE, JSON.stringify(cache));
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function minifyEmailId(realId, cache) {
|
|
347
|
+
if (!realId) return realId;
|
|
348
|
+
const hash = createHash("md5").update(realId).digest("hex").slice(0, 6);
|
|
349
|
+
const shortId = `msg_${hash}`;
|
|
350
|
+
cache[shortId] = realId;
|
|
351
|
+
return shortId;
|
|
352
|
+
}
|
|
353
|
+
function resolveEmailId(shortId) {
|
|
354
|
+
if (!shortId) return shortId;
|
|
355
|
+
const cache = loadIdCache();
|
|
356
|
+
return cache[shortId] || shortId;
|
|
357
|
+
}
|
|
358
|
+
function stripTags(html) {
|
|
359
|
+
if (!html) return "";
|
|
360
|
+
let text = html.replace(/<(style|script|head)[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
361
|
+
text = text.replace(
|
|
362
|
+
/<\/?(p|div|br|hr|tr|li|h[1-6]|blockquote)\b[^>]*>/gi,
|
|
363
|
+
"\n"
|
|
364
|
+
);
|
|
365
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
366
|
+
return text.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
|
|
367
|
+
}
|
|
368
|
+
function removeNestedQuotes(text) {
|
|
369
|
+
if (!text) return "";
|
|
370
|
+
const patterns = [
|
|
371
|
+
/_{10,}/m,
|
|
372
|
+
/^From:\s.*?\n(?:.*\n){0,5}?Sent:\s/m,
|
|
373
|
+
/-----Original Message-----/m,
|
|
374
|
+
/On .{1,200}? wrote:/m,
|
|
375
|
+
/^Original Message$/m
|
|
376
|
+
];
|
|
377
|
+
let earliestCut = text.length;
|
|
378
|
+
for (const pattern of patterns) {
|
|
379
|
+
const match = pattern.exec(text);
|
|
380
|
+
if (match && match.index < earliestCut) {
|
|
381
|
+
earliestCut = match.index;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return text.substring(0, earliestCut).trim();
|
|
385
|
+
}
|
|
386
|
+
function getUniqueFilepath(saveDir, filename) {
|
|
387
|
+
let filepath = join2(saveDir, filename);
|
|
388
|
+
let counter = 1;
|
|
389
|
+
const parts = filename.split(".");
|
|
390
|
+
const ext = parts.length > 1 ? `.${parts.pop()}` : "";
|
|
391
|
+
const stem = parts.join(".");
|
|
392
|
+
while (existsSync2(filepath)) {
|
|
393
|
+
filepath = join2(saveDir, `${stem}_${counter}${ext}`);
|
|
394
|
+
counter++;
|
|
395
|
+
}
|
|
396
|
+
return filepath;
|
|
397
|
+
}
|
|
398
|
+
async function search_and_fetch_emails(args) {
|
|
399
|
+
const apiKey = await getCloudAuthToken();
|
|
400
|
+
const realEmailId = args.email_id ? resolveEmailId(args.email_id) : void 0;
|
|
401
|
+
const payload = {
|
|
402
|
+
email_id: realEmailId,
|
|
403
|
+
sender: args.sender,
|
|
404
|
+
subject: args.subject,
|
|
405
|
+
has_attachments: args.has_attachments,
|
|
406
|
+
attachment_name: args.attachment_name,
|
|
407
|
+
is_unread: args.is_unread,
|
|
408
|
+
days_ago: args.days_ago,
|
|
409
|
+
folder: args.folder,
|
|
410
|
+
limit: args.limit ?? 10,
|
|
411
|
+
offset: args.offset ?? 0
|
|
412
|
+
};
|
|
413
|
+
Object.keys(payload).forEach(
|
|
414
|
+
(k) => payload[k] === void 0 && delete payload[k]
|
|
415
|
+
);
|
|
416
|
+
const res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: {
|
|
419
|
+
Authorization: `Bearer ${apiKey}`,
|
|
420
|
+
"Content-Type": "application/json"
|
|
421
|
+
},
|
|
422
|
+
body: JSON.stringify(payload)
|
|
423
|
+
});
|
|
424
|
+
if (res.status === 401) {
|
|
425
|
+
DesktopAuthManager.clearApiKey();
|
|
426
|
+
throw new Error(
|
|
427
|
+
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate."
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (!res.ok) throw new Error(`Cloud search failed: ${await res.text()}`);
|
|
431
|
+
const data = await res.json();
|
|
432
|
+
const cache = loadIdCache();
|
|
433
|
+
if (data.type === "previews") {
|
|
434
|
+
const previews = data.previews || [];
|
|
435
|
+
if (!previews.length)
|
|
436
|
+
return {
|
|
437
|
+
content: [
|
|
438
|
+
{
|
|
439
|
+
type: "text",
|
|
440
|
+
text: "No emails found matching your search criteria."
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
};
|
|
444
|
+
const lines = [
|
|
445
|
+
`Found ${previews.length} email(s). Here are the previews:`,
|
|
446
|
+
""
|
|
447
|
+
];
|
|
448
|
+
for (const p of previews) {
|
|
449
|
+
const shortId = minifyEmailId(p.id, cache);
|
|
450
|
+
const attFlag = p.has_attachments ? "\u{1F4CE} (Has Attachments)" : "";
|
|
451
|
+
const unreadFlag = p.is_read === false ? "\u{1F7E2} [UNREAD]" : "";
|
|
452
|
+
lines.push(
|
|
453
|
+
`- **ID**: \`${shortId}\`
|
|
454
|
+
**Subject**: ${p.subject} ${attFlag} ${unreadFlag}
|
|
455
|
+
**From**: ${p.sender_name} <${p.sender_email}>
|
|
456
|
+
**Date**: ${p.received_datetime}
|
|
457
|
+
**Preview**: ${p.preview_text}
|
|
458
|
+
`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
saveIdCache(cache);
|
|
462
|
+
lines.push(
|
|
463
|
+
"\u26A0\uFE0F **ACTION REQUIRED**: To read the full body of an email and download its attachments, call this tool again and provide the exact `email_id`."
|
|
464
|
+
);
|
|
465
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
466
|
+
}
|
|
467
|
+
if (data.type === "full_email") {
|
|
468
|
+
const full = data.full_email || {};
|
|
469
|
+
const shortTargetId = minifyEmailId(full.id || "unknown_id", cache);
|
|
470
|
+
saveIdCache(cache);
|
|
471
|
+
const baseDir = args.working_directory && existsSync2(args.working_directory) ? args.working_directory : tmpdir();
|
|
472
|
+
const saveDir = join2(
|
|
473
|
+
baseDir,
|
|
474
|
+
args.working_directory ? "adeu_attachments" : "adeu_downloads",
|
|
475
|
+
shortTargetId
|
|
476
|
+
);
|
|
477
|
+
mkdirSync2(saveDir, { recursive: true });
|
|
478
|
+
async function processAttachments(msg) {
|
|
479
|
+
const localFiles = [];
|
|
480
|
+
for (const att of msg.attachments || []) {
|
|
481
|
+
if (att.base64_data) {
|
|
482
|
+
try {
|
|
483
|
+
const filepath = getUniqueFilepath(
|
|
484
|
+
saveDir,
|
|
485
|
+
att.filename || "unnamed_file"
|
|
486
|
+
);
|
|
487
|
+
writeFileSync2(filepath, Buffer.from(att.base64_data, "base64"));
|
|
488
|
+
localFiles.push(filepath);
|
|
489
|
+
delete att.base64_data;
|
|
490
|
+
} catch (e) {
|
|
491
|
+
console.error(`Failed to save attachment ${att.filename}`, e);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return localFiles;
|
|
496
|
+
}
|
|
497
|
+
const targetFiles = await processAttachments(full);
|
|
498
|
+
const lines = [
|
|
499
|
+
`# Email Thread: ${full.subject}`,
|
|
500
|
+
"",
|
|
501
|
+
"## Target Message (Newest):",
|
|
502
|
+
`**From**: ${full.sender_name} <${full.sender_email}>`,
|
|
503
|
+
`**Date**: ${full.received_datetime}`
|
|
504
|
+
];
|
|
505
|
+
if (targetFiles.length) {
|
|
506
|
+
lines.push("**Attachments Saved Locally**:");
|
|
507
|
+
targetFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
|
|
508
|
+
}
|
|
509
|
+
const cleanBody = removeNestedQuotes(stripTags(full.body_html || ""));
|
|
510
|
+
lines.push(`**Body**:
|
|
511
|
+
\`\`\`
|
|
512
|
+
${cleanBody}
|
|
513
|
+
\`\`\`
|
|
514
|
+
`);
|
|
515
|
+
if (full.is_thread && full.messages?.length) {
|
|
516
|
+
lines.push("## Previous Messages in Thread (Historical Context):");
|
|
517
|
+
for (let i = 0; i < full.messages.length; i++) {
|
|
518
|
+
const histMsg = full.messages[i];
|
|
519
|
+
const histFiles = await processAttachments(histMsg);
|
|
520
|
+
lines.push(
|
|
521
|
+
`### Message -${i + 1} (Older)
|
|
522
|
+
**From**: ${histMsg.sender_name} <${histMsg.sender_email}>
|
|
523
|
+
**Date**: ${histMsg.received_datetime}`
|
|
524
|
+
);
|
|
525
|
+
if (histFiles.length) {
|
|
526
|
+
lines.push("**Attachments Saved Locally**:");
|
|
527
|
+
histFiles.forEach((f) => lines.push(`- \u{1F4CE} \`${f}\``));
|
|
528
|
+
}
|
|
529
|
+
lines.push(
|
|
530
|
+
`**Body**:
|
|
531
|
+
\`\`\`
|
|
532
|
+
${removeNestedQuotes(stripTags(histMsg.body_html || ""))}
|
|
533
|
+
\`\`\`
|
|
534
|
+
`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
isError: true,
|
|
542
|
+
content: [{ type: "text", text: "Unknown response format from backend." }]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function create_email_draft(args) {
|
|
546
|
+
const apiKey = await getCloudAuthToken();
|
|
547
|
+
if (!args.reply_to_email_id && (!args.subject || !args.to_recipients)) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
"You must provide either 'reply_to_email_id' OR both 'subject' and 'to_recipients'."
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const formData = new FormData();
|
|
553
|
+
formData.append("body_markdown", args.body_markdown);
|
|
554
|
+
if (args.reply_to_email_id) {
|
|
555
|
+
formData.append(
|
|
556
|
+
"reply_to_email_id",
|
|
557
|
+
resolveEmailId(args.reply_to_email_id)
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (args.subject) formData.append("subject", args.subject);
|
|
561
|
+
if (args.to_recipients) {
|
|
562
|
+
const recips = typeof args.to_recipients === "string" ? JSON.parse(args.to_recipients) : args.to_recipients;
|
|
563
|
+
formData.append("to_recipients", JSON.stringify(recips));
|
|
564
|
+
}
|
|
565
|
+
if (args.attachment_paths) {
|
|
566
|
+
const paths = typeof args.attachment_paths === "string" ? JSON.parse(args.attachment_paths) : args.attachment_paths;
|
|
567
|
+
for (const p of paths) {
|
|
568
|
+
const buf = readFileSync2(p);
|
|
569
|
+
const filename = p.split(/[/\\]/).pop();
|
|
570
|
+
formData.append("files", new Blob([buf]), filename);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const res = await fetch(`${BACKEND_URL}/api/v1/emails/drafts/new`, {
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
576
|
+
body: formData
|
|
577
|
+
});
|
|
578
|
+
if (res.status === 401) {
|
|
579
|
+
DesktopAuthManager.clearApiKey();
|
|
580
|
+
throw new Error(
|
|
581
|
+
"Authentication expired. Please call `login_to_adeu_cloud`."
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (!res.ok)
|
|
585
|
+
throw new Error(`Cloud draft creation failed: ${await res.text()}`);
|
|
586
|
+
const data = await res.json();
|
|
587
|
+
return {
|
|
588
|
+
content: [
|
|
589
|
+
{
|
|
590
|
+
type: "text",
|
|
591
|
+
text: `Successfully created email draft! Draft ID: ${data.id}`
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
163
597
|
// src/index.ts
|
|
164
598
|
function readFileBytesOrThrow(filePath) {
|
|
165
599
|
try {
|
|
166
|
-
return
|
|
600
|
+
return readFileSync3(filePath);
|
|
167
601
|
} catch (err) {
|
|
168
602
|
if (err.code === "ENOENT") {
|
|
169
603
|
throw new Error(`File not found: ${filePath}`);
|
|
@@ -176,7 +610,7 @@ var READ_DOCX_TAIL = "Modes:\n- 'full' (default): paginated body content. Use pa
|
|
|
176
610
|
var PROCESS_BATCH_COMMON_DESC = "Applies a batch of edits and review actions to a DOCX.\n\nAll changes evaluate against the ORIGINAL document state \u2014 do not chain dependent edits within one batch (e.g. rename X to Y, then modify Y). Apply the rename first, then send a second batch.\n\n";
|
|
177
611
|
var PROCESS_BATCH_OPERATIONS_DESC = "Each item in `changes` must specify a `type`:\n1. 'modify': Search-and-replace. `target_text` must uniquely match \u2014 include surrounding context if the phrase is ambiguous. `new_text` supports Markdown: '# Heading 1' through '###### Heading 6', '**bold**', '_italic_', and '\\n\\n' to split into multiple paragraphs. Empty `new_text` deletes. Do NOT write CriticMarkup tags ({++, {--, {>>) manually \u2014 use the `comment` parameter for comments.\n2. 'accept' / 'reject': Finalize or revert a tracked change by `target_id` (e.g. 'Chg:12').\n3. 'reply': Reply to a comment by `target_id` (e.g. 'Com:5') with `text`.\n4. 'insert_row' / 'delete_row': Table edits. Disk mode only \u2014 not supported on Live Word canvas.\n\nID VOLATILITY: 'Chg:N' and 'Com:N' shift between document states. Always call `read_docx` immediately before any accept/reject/reply \u2014 do not reuse IDs from earlier in the conversation.\n\n`author_name` is used for attribution on all tracked changes and comments, in both disk and Live Word modes.";
|
|
178
612
|
var DIFF_DOCX_DESC = "Compares two DOCX files and returns a unified diff of their text content. Useful for analyzing differences between versions before editing.";
|
|
179
|
-
var server = new
|
|
613
|
+
var server = new Server2(
|
|
180
614
|
{
|
|
181
615
|
name: "adeu-redlining-service",
|
|
182
616
|
version: "1.0.0"
|
|
@@ -341,6 +775,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
341
775
|
},
|
|
342
776
|
required: ["file_path"]
|
|
343
777
|
}
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
name: "login_to_adeu_cloud",
|
|
781
|
+
description: "Logs the user into the Adeu Cloud backend. Securely opens a browser window for authentication.",
|
|
782
|
+
inputSchema: { type: "object", properties: {} }
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: "logout_of_adeu_cloud",
|
|
786
|
+
description: "Logs out of the Adeu Cloud backend by clearing the local API key.",
|
|
787
|
+
inputSchema: { type: "object", properties: {} }
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: "search_and_fetch_emails",
|
|
791
|
+
description: "Searches the user's live email inbox. By default, searches only the Inbox folder. Returns a list of lightweight previews. Call again with `email_id` to fetch the full body and download attachments.",
|
|
792
|
+
inputSchema: {
|
|
793
|
+
type: "object",
|
|
794
|
+
properties: {
|
|
795
|
+
sender: { type: "string" },
|
|
796
|
+
subject: { type: "string" },
|
|
797
|
+
has_attachments: { type: "boolean" },
|
|
798
|
+
attachment_name: { type: "string" },
|
|
799
|
+
is_unread: { type: "boolean" },
|
|
800
|
+
days_ago: { type: "number" },
|
|
801
|
+
folder: { type: "string", enum: ["inbox", "sent", "all"] },
|
|
802
|
+
limit: { type: "number", default: 10 },
|
|
803
|
+
offset: { type: "number", default: 0 },
|
|
804
|
+
email_id: { type: "string" },
|
|
805
|
+
working_directory: { type: "string" }
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: "create_email_draft",
|
|
811
|
+
description: "Creates an email draft in the user's native draft box. Provide `reply_to_email_id` to reply, or `subject` and `to_recipients` for a new email.",
|
|
812
|
+
inputSchema: {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties: {
|
|
815
|
+
body_markdown: { type: "string" },
|
|
816
|
+
reply_to_email_id: { type: "string" },
|
|
817
|
+
subject: { type: "string" },
|
|
818
|
+
to_recipients: { type: "array", items: { type: "string" } },
|
|
819
|
+
attachment_paths: { type: "array", items: { type: "string" } }
|
|
820
|
+
},
|
|
821
|
+
required: ["body_markdown"]
|
|
822
|
+
}
|
|
344
823
|
}
|
|
345
824
|
]
|
|
346
825
|
};
|
|
@@ -510,6 +989,18 @@ ${result.reportText}`
|
|
|
510
989
|
]
|
|
511
990
|
};
|
|
512
991
|
}
|
|
992
|
+
if (name === "login_to_adeu_cloud") {
|
|
993
|
+
return await login_to_adeu_cloud();
|
|
994
|
+
}
|
|
995
|
+
if (name === "logout_of_adeu_cloud") {
|
|
996
|
+
return await logout_of_adeu_cloud();
|
|
997
|
+
}
|
|
998
|
+
if (name === "search_and_fetch_emails") {
|
|
999
|
+
return await search_and_fetch_emails(args || {});
|
|
1000
|
+
}
|
|
1001
|
+
if (name === "create_email_draft") {
|
|
1002
|
+
return await create_email_draft(args || {});
|
|
1003
|
+
}
|
|
513
1004
|
throw new Error(`Unknown tool: ${name}`);
|
|
514
1005
|
} catch (error) {
|
|
515
1006
|
return {
|