@adeu/mcp-server 1.15.1 → 1.16.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 +156 -57
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +293 -180
- package/src/mcp.bugs.test.ts +23 -0
- package/src/mcp.schema-gaps.test.ts +384 -0
- package/src/response-builders.ts +118 -50
- package/src/response_builders.test.ts +266 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adeu/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"mcpName": "ai.adeu/adeu",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"type": "module",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@adeu/core": "^1.
|
|
34
|
+
"@adeu/core": "^1.16.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
36
|
"@modelcontextprotocol/ext-apps": "^1.0.0",
|
|
37
37
|
"zod": "^3.23.8"
|
package/src/index.ts
CHANGED
|
@@ -42,17 +42,17 @@ function readFileBytesOrThrow(filePath: string): Buffer {
|
|
|
42
42
|
if (err.code === "ENOENT") {
|
|
43
43
|
throw new Error(
|
|
44
44
|
`File not found: ${filePath}.\n` +
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
`If you are running in a sandboxed/containerized environment (such as Claude Desktop or another containerized client), ` +
|
|
46
|
+
`the host application or MCP server may not have direct access to your local workspace files.\n` +
|
|
47
|
+
`You can resolve this by installing and running the local 'adeu' CLI tool directly within your environment.\n` +
|
|
48
|
+
`Here is how the MCP tools map to their CLI equivalents:\n` +
|
|
49
|
+
`- read_docx -> adeu extract ${filePath}\n` +
|
|
50
|
+
`- process_document_batch -> adeu apply ${filePath}\n` +
|
|
51
|
+
`- diff_docx_files -> adeu diff ${filePath} <modified_path>\n` +
|
|
52
|
+
`- accept_all_changes -> adeu accept-all ${filePath}\n\n` +
|
|
53
|
+
`To run the local tool, install it via:\n` +
|
|
54
|
+
` uv tool install adeu\n` +
|
|
55
|
+
`and run the mapped CLI command directly in your terminal.`,
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
throw err;
|
|
@@ -83,10 +83,10 @@ const READ_DOCX_TAIL =
|
|
|
83
83
|
const PROCESS_BATCH_COMMON_DESC =
|
|
84
84
|
"Applies a batch of edits and review actions to a DOCX.\n\nAll changes evaluate against the ORIGINAL document state — 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";
|
|
85
85
|
const PROCESS_BATCH_OPERATIONS_DESC =
|
|
86
|
-
"Each item in `changes` must specify a `type`:\n1. 'modify': Search-and-replace. `target_text` must uniquely
|
|
86
|
+
"Each item in `changes` must specify a `type`:\n1. 'modify': Search-and-replace. By default `target_text` must match uniquely (`match_mode`:'strict') — add surrounding context to disambiguate, or set `match_mode`:'first'/'all' to edit the first or every occurrence. Set `regex`:true to treat `target_text` as a regular expression (capture groups available in `new_text` as $1, $2…). `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 — 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 — 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 — 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.";
|
|
87
87
|
|
|
88
88
|
const DIFF_DOCX_DESC =
|
|
89
|
-
"Compares two DOCX files and returns a
|
|
89
|
+
"Compares two DOCX files and returns a compact `@@ Word Patch @@` diff — Adeu's token-level, sub-word patch format — of their text content. Useful for analyzing differences between versions before editing.";
|
|
90
90
|
|
|
91
91
|
const gitSha = process.env.GIT_SHA || "unknown";
|
|
92
92
|
const packageVersion = process.env.PACKAGE_VERSION || "unknown";
|
|
@@ -95,7 +95,9 @@ const buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
|
|
|
95
95
|
// --- Scope Configuration ---
|
|
96
96
|
const args = process.argv.slice(2);
|
|
97
97
|
const scopeIdx = args.indexOf("--scope");
|
|
98
|
-
const requestedScope = (
|
|
98
|
+
const requestedScope = (
|
|
99
|
+
scopeIdx !== -1 ? args[scopeIdx + 1] : "all"
|
|
100
|
+
).toLowerCase();
|
|
99
101
|
const isDocxOnly = requestedScope === "docx";
|
|
100
102
|
|
|
101
103
|
// --- Server Setup ---
|
|
@@ -108,7 +110,9 @@ const server = new McpServer({
|
|
|
108
110
|
const originalRegisterTool = server.registerTool.bind(server);
|
|
109
111
|
server.registerTool = (name: string, schema: any, handler?: any) => {
|
|
110
112
|
if (schema && typeof schema === "object") {
|
|
111
|
-
|
|
113
|
+
// Idempotent: UI tools route through BOTH this wrapper and the
|
|
114
|
+
// registerAppTool wrapper, so guard against stamping the tag twice.
|
|
115
|
+
if (schema.description && !schema.description.includes(buildTag.trim())) {
|
|
112
116
|
schema.description = schema.description.trim() + buildTag;
|
|
113
117
|
}
|
|
114
118
|
}
|
|
@@ -116,7 +120,12 @@ server.registerTool = (name: string, schema: any, handler?: any) => {
|
|
|
116
120
|
};
|
|
117
121
|
|
|
118
122
|
// Wrap registerAppTool to inject buildTag into descriptions
|
|
119
|
-
const registerAppTool: typeof origRegisterAppTool = (
|
|
123
|
+
const registerAppTool: typeof origRegisterAppTool = (
|
|
124
|
+
mcpServer,
|
|
125
|
+
name,
|
|
126
|
+
schema,
|
|
127
|
+
handler,
|
|
128
|
+
) => {
|
|
120
129
|
if (schema && typeof schema === "object") {
|
|
121
130
|
if (schema.description) {
|
|
122
131
|
schema.description = schema.description.trim() + buildTag;
|
|
@@ -226,10 +235,12 @@ registerAppTool(
|
|
|
226
235
|
),
|
|
227
236
|
page: z
|
|
228
237
|
.union([z.number(), z.string()])
|
|
229
|
-
.
|
|
230
|
-
.describe(
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
.optional()
|
|
239
|
+
.describe(
|
|
240
|
+
"Without `search_query`: 1-indexed document page to display (defaults to 1). With `search_query`: restricts matches to that document page (defaults to searching all pages; pass `page='all'` to be explicit).",
|
|
241
|
+
),
|
|
242
|
+
outline_max_level: z.coerce
|
|
243
|
+
.number()
|
|
233
244
|
.default(2)
|
|
234
245
|
.describe("For mode='outline' only: cap on heading depth."),
|
|
235
246
|
outline_verbose: z
|
|
@@ -239,11 +250,15 @@ registerAppTool(
|
|
|
239
250
|
search_query: z
|
|
240
251
|
.string()
|
|
241
252
|
.optional()
|
|
242
|
-
.describe(
|
|
253
|
+
.describe(
|
|
254
|
+
"The substring or regex pattern to search for. When provided, filters results to matching paragraphs.",
|
|
255
|
+
),
|
|
243
256
|
search_regex: z
|
|
244
257
|
.boolean()
|
|
245
258
|
.default(false)
|
|
246
|
-
.describe(
|
|
259
|
+
.describe(
|
|
260
|
+
"Set to true to interpret search_query as a regular expression.",
|
|
261
|
+
),
|
|
247
262
|
search_case_sensitive: z
|
|
248
263
|
.boolean()
|
|
249
264
|
.default(true)
|
|
@@ -267,7 +282,12 @@ registerAppTool(
|
|
|
267
282
|
|
|
268
283
|
if (mode === "outline") {
|
|
269
284
|
const doc = await DocumentObject.load(buf);
|
|
270
|
-
const extract_res = _extractTextFromDoc(
|
|
285
|
+
const extract_res = _extractTextFromDoc(
|
|
286
|
+
doc,
|
|
287
|
+
clean_view,
|
|
288
|
+
true,
|
|
289
|
+
true,
|
|
290
|
+
) as {
|
|
271
291
|
text: string;
|
|
272
292
|
paragraph_offsets: Map<any, [number, number]>;
|
|
273
293
|
};
|
|
@@ -284,14 +304,29 @@ registerAppTool(
|
|
|
284
304
|
|
|
285
305
|
const text = await extractTextFromBuffer(buf, clean_view);
|
|
286
306
|
if (search_query !== undefined && search_query !== null) {
|
|
287
|
-
|
|
307
|
+
// In search mode, undefined `page` means "search all document pages".
|
|
308
|
+
const res = build_search_response(
|
|
309
|
+
text,
|
|
310
|
+
search_query,
|
|
311
|
+
search_regex,
|
|
312
|
+
search_case_sensitive,
|
|
313
|
+
page,
|
|
314
|
+
file_path,
|
|
315
|
+
);
|
|
288
316
|
return res as any;
|
|
289
317
|
}
|
|
318
|
+
// In non-search mode, `page` defaults to 1 (show document page 1).
|
|
319
|
+
const resolvedPage =
|
|
320
|
+
page === undefined || page === null
|
|
321
|
+
? 1
|
|
322
|
+
: typeof page === "number"
|
|
323
|
+
? page
|
|
324
|
+
: parseInt(String(page), 10) || 1;
|
|
290
325
|
if (mode === "appendix") {
|
|
291
|
-
const res = build_appendix_response(text,
|
|
326
|
+
const res = build_appendix_response(text, resolvedPage, file_path);
|
|
292
327
|
return res as any;
|
|
293
328
|
}
|
|
294
|
-
const res = build_paginated_response(text,
|
|
329
|
+
const res = build_paginated_response(text, resolvedPage, file_path);
|
|
295
330
|
return res as any;
|
|
296
331
|
} catch (e: any) {
|
|
297
332
|
return {
|
|
@@ -311,6 +346,69 @@ registerAppTool(
|
|
|
311
346
|
// 3. HEADLESS TOOLS (No UI)
|
|
312
347
|
// ==========================================
|
|
313
348
|
|
|
349
|
+
// Typed shape for a single `process_document_batch` change. This makes the six
|
|
350
|
+
// DocumentChange variants — and the modify-only `match_mode`/`regex` options —
|
|
351
|
+
// discoverable from the tool schema itself, instead of prose alone. A bare
|
|
352
|
+
// string is still accepted (and normalized in-handler) so double-serialized
|
|
353
|
+
// payloads from some LLM clients keep working; only `type` is required, all
|
|
354
|
+
// other fields are optional, and unknown keys pass through untouched.
|
|
355
|
+
const CHANGE_ITEM_SCHEMA = z
|
|
356
|
+
.object({
|
|
357
|
+
type: z
|
|
358
|
+
.enum(["modify", "accept", "reject", "reply", "insert_row", "delete_row"])
|
|
359
|
+
.describe(
|
|
360
|
+
"Change kind: 'modify' (search-and-replace), 'accept'/'reject' (resolve a tracked change by id), 'reply' (reply to a comment by id), 'insert_row'/'delete_row' (table edits; disk mode only).",
|
|
361
|
+
),
|
|
362
|
+
target_text: z
|
|
363
|
+
.string()
|
|
364
|
+
.optional()
|
|
365
|
+
.describe(
|
|
366
|
+
"modify / insert_row / delete_row: the existing text to locate (interpreted as a regex when regex=true).",
|
|
367
|
+
),
|
|
368
|
+
new_text: z
|
|
369
|
+
.string()
|
|
370
|
+
.optional()
|
|
371
|
+
.describe(
|
|
372
|
+
"modify: replacement text. Supports Markdown (headings, **bold**, _italic_, '\\n\\n' paragraph splits); empty string deletes. Regex capture groups are available as $1, $2…",
|
|
373
|
+
),
|
|
374
|
+
target_id: z
|
|
375
|
+
.string()
|
|
376
|
+
.optional()
|
|
377
|
+
.describe(
|
|
378
|
+
"accept / reject / reply: the 'Chg:N' or 'Com:N' id taken from a fresh read_docx.",
|
|
379
|
+
),
|
|
380
|
+
text: z.string().optional().describe("reply: the reply body."),
|
|
381
|
+
comment: z
|
|
382
|
+
.string()
|
|
383
|
+
.optional()
|
|
384
|
+
.describe(
|
|
385
|
+
"modify / accept / reject: attach a margin comment to the change (no manual CriticMarkup).",
|
|
386
|
+
),
|
|
387
|
+
match_mode: z
|
|
388
|
+
.enum(["strict", "first", "all"])
|
|
389
|
+
.optional()
|
|
390
|
+
.describe(
|
|
391
|
+
"modify only: 'strict' (default — target must match uniquely), 'first' (first occurrence), or 'all' (every occurrence).",
|
|
392
|
+
),
|
|
393
|
+
regex: z
|
|
394
|
+
.boolean()
|
|
395
|
+
.optional()
|
|
396
|
+
.describe(
|
|
397
|
+
"modify only: treat target_text as a regular expression (default false).",
|
|
398
|
+
),
|
|
399
|
+
position: z
|
|
400
|
+
.enum(["above", "below"])
|
|
401
|
+
.optional()
|
|
402
|
+
.describe(
|
|
403
|
+
"insert_row: place the new row above or below the matched row.",
|
|
404
|
+
),
|
|
405
|
+
cells: z
|
|
406
|
+
.array(z.string())
|
|
407
|
+
.optional()
|
|
408
|
+
.describe("insert_row: the cell values for the new row, left to right."),
|
|
409
|
+
})
|
|
410
|
+
.passthrough();
|
|
411
|
+
|
|
314
412
|
server.registerTool(
|
|
315
413
|
"process_document_batch",
|
|
316
414
|
{
|
|
@@ -323,8 +421,10 @@ server.registerTool(
|
|
|
323
421
|
.string()
|
|
324
422
|
.describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
|
|
325
423
|
changes: z
|
|
326
|
-
.array(z.
|
|
327
|
-
.describe(
|
|
424
|
+
.array(z.union([z.string(), CHANGE_ITEM_SCHEMA]))
|
|
425
|
+
.describe(
|
|
426
|
+
"Ordered list of changes to apply. Each item is an object carrying a `type` discriminator plus that type's fields (see the per-field docs and the tool description). All items evaluate against the ORIGINAL document state.",
|
|
427
|
+
),
|
|
328
428
|
output_path: z.string().optional().describe("Optional output path."),
|
|
329
429
|
dry_run: z
|
|
330
430
|
.boolean()
|
|
@@ -516,7 +616,7 @@ server.registerTool(
|
|
|
516
616
|
"finalize_document",
|
|
517
617
|
{
|
|
518
618
|
description:
|
|
519
|
-
"Prepares a document for external distribution or e-signature.",
|
|
619
|
+
"Prepares a document for external distribution or e-signature. Note: in this zero-dependency environment, protection_mode='encrypt' is unsupported and falls back to a native read-only lock; export_pdf and password are ignored.",
|
|
520
620
|
inputSchema: {
|
|
521
621
|
file_path: z.string().describe("Absolute path to the DOCX file."),
|
|
522
622
|
output_path: z.string().optional().describe("Optional output path."),
|
|
@@ -533,7 +633,9 @@ server.registerTool(
|
|
|
533
633
|
protection_mode: z
|
|
534
634
|
.enum(["read_only", "encrypt"])
|
|
535
635
|
.optional()
|
|
536
|
-
.describe(
|
|
636
|
+
.describe(
|
|
637
|
+
"Native OOXML document locking. Note: 'encrypt' is unsupported in this zero-dependency build and falls back to 'read_only'.",
|
|
638
|
+
),
|
|
537
639
|
password: z.string().optional().describe("Ignored in this environment."),
|
|
538
640
|
author: z
|
|
539
641
|
.string()
|
|
@@ -595,145 +697,145 @@ server.registerTool(
|
|
|
595
697
|
);
|
|
596
698
|
|
|
597
699
|
if (!isDocxOnly) {
|
|
598
|
-
registerAppTool(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
);
|
|
700
|
+
registerAppTool(
|
|
701
|
+
server,
|
|
702
|
+
"search_and_fetch_emails",
|
|
703
|
+
{
|
|
704
|
+
title: "Search & Fetch Emails",
|
|
705
|
+
description:
|
|
706
|
+
"Searches the user's live email inbox via the Adeu cloud backend.\n\n" +
|
|
707
|
+
"TWO MODES:\n" +
|
|
708
|
+
"1. Search mode (no `email_id`): returns up to `limit` lightweight previews. Use filters (`sender`, `subject`, `is_unread`, `days_ago`, `folder`, `has_attachments`, `attachment_name`) to narrow down.\n" +
|
|
709
|
+
"2. Fetch mode (with `email_id`): returns the full email body, thread history, and downloads attachments under `max_attachment_size_mb` to the local disk.\n\n" +
|
|
710
|
+
"AUTO-ESCALATION: If a search returns exactly one preview, the backend automatically fetches the full email in the same call. Plan around the response shape — check the `type` field (`previews` vs `full_email`) before assuming.\n\n" +
|
|
711
|
+
"EMAIL ID FORMATS (`email_id` parameter accepts any of):\n" +
|
|
712
|
+
"- `msg_<6 chars>` — short ID returned by previews on THIS machine. NOT portable across machines or sessions; the local cache holds the most recent 1000. If you reference one that's been evicted, the tool returns a StaleShortIdError telling you to re-search.\n" +
|
|
713
|
+
"- `adeu_<numeric>` — server-side reference for emails Adeu has previously processed. Portable across machines and sessions for the same authenticated user.\n" +
|
|
714
|
+
"- Raw provider ID (Gmail/Outlook native ID) — works if you have it, but you usually won't.\n\n" +
|
|
715
|
+
"FOLDER DEFAULT: omitting `folder` searches the Inbox only (matching what the user sees in their mail client). Use `folder='sent'` for sent items, `folder='all'` to include Deleted Items, Drafts, and other folders.\n\n" +
|
|
716
|
+
"ATTACHMENTS: attachments larger than `max_attachment_size_mb` (default 10) are listed in the response but NOT downloaded — raise the cap if you need them. Always set `working_directory` when calling from a project so attachments land alongside the user's other files.",
|
|
717
|
+
inputSchema: z.object({
|
|
718
|
+
sender: z.string().optional(),
|
|
719
|
+
subject: z.string().optional(),
|
|
720
|
+
has_attachments: z.boolean().optional(),
|
|
721
|
+
attachment_name: z.string().optional(),
|
|
722
|
+
is_unread: z.boolean().optional(),
|
|
723
|
+
days_ago: z.coerce.number().optional(),
|
|
724
|
+
folder: z.enum(["inbox", "sent", "all"]).optional(),
|
|
725
|
+
limit: z.coerce.number().default(10),
|
|
726
|
+
offset: z.coerce.number().default(0),
|
|
727
|
+
email_id: z.string().optional(),
|
|
728
|
+
working_directory: z.string().optional(),
|
|
729
|
+
mailbox_address: z
|
|
730
|
+
.string()
|
|
731
|
+
.optional()
|
|
732
|
+
.describe("Optional target mailbox email address to search within."),
|
|
733
|
+
task_id: z
|
|
734
|
+
.string()
|
|
735
|
+
.optional()
|
|
736
|
+
.describe("If resuming a pending check, provide the task ID here."),
|
|
737
|
+
max_attachment_size_mb: z.coerce
|
|
738
|
+
.number()
|
|
739
|
+
.optional()
|
|
740
|
+
.describe(
|
|
741
|
+
"Maximum attachment size in MB to download (default 10). Attachments larger than this are listed in the response but not downloaded. Raise this to fetch large files.",
|
|
742
|
+
),
|
|
743
|
+
}),
|
|
744
|
+
_meta: { ui: { resourceUri: EMAIL_UI_URI } },
|
|
745
|
+
},
|
|
746
|
+
async (args) => {
|
|
747
|
+
try {
|
|
748
|
+
return (await search_and_fetch_emails(args)) as any;
|
|
749
|
+
} catch (e: any) {
|
|
750
|
+
return {
|
|
751
|
+
isError: true,
|
|
752
|
+
content: [{ type: "text", text: e.message }],
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
);
|
|
655
757
|
|
|
656
|
-
server.registerTool(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
);
|
|
758
|
+
server.registerTool(
|
|
759
|
+
"login_to_adeu_cloud",
|
|
760
|
+
{
|
|
761
|
+
description:
|
|
762
|
+
"Logs the user into Adeu Cloud. Opens a browser window for SSO authentication.\n\n" +
|
|
763
|
+
"IMPORTANT — login is user-level, not account-level:\n" +
|
|
764
|
+
"- An Adeu user can have multiple linked provider accounts (Microsoft, Google) and multiple mailboxes (personal + shared/delegated). One linked account is marked primary.\n" +
|
|
765
|
+
"- Signing in through ANY of the user's linked accounts authenticates the same Adeu user. Once logged in, the session can read from and draft in ALL of that user's linked accounts and ALL of their mailboxes — not just the one used to sign in.\n" +
|
|
766
|
+
"- The choice of which provider account to sign in through is purely an SSO mechanism; it does not select a 'current account' for the session.\n\n" +
|
|
767
|
+
"When the user asks which accounts or mailboxes are available, call `list_available_mailboxes` rather than naming a single account from the login response.",
|
|
768
|
+
},
|
|
769
|
+
async () => {
|
|
770
|
+
try {
|
|
771
|
+
return (await login_to_adeu_cloud()) as any;
|
|
772
|
+
} catch (e: any) {
|
|
773
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
);
|
|
675
777
|
|
|
676
|
-
server.registerTool(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
},
|
|
686
|
-
);
|
|
687
|
-
server.registerTool(
|
|
688
|
-
"create_email_draft",
|
|
689
|
-
{
|
|
690
|
-
description:
|
|
691
|
-
"Creates an email draft in the user's native draft box (Outlook Drafts or Gmail Drafts).\n\n" +
|
|
692
|
-
"TWO MODES:\n" +
|
|
693
|
-
"1. Reply mode: pass `reply_to_email_id` to create a threaded reply. The draft inherits subject, recipients, and threading headers from the original — do NOT pass `subject` or `to_recipients`.\n" +
|
|
694
|
-
"2. New email mode: omit `reply_to_email_id` and pass BOTH `subject` and `to_recipients`.\n\n" +
|
|
695
|
-
"`reply_to_email_id` accepts the same ID formats as search_and_fetch_emails (`msg_*` short IDs, `adeu_*` references, or raw provider IDs). Short IDs are validated against the local cache before the call; stale ones fail fast with a clear error telling you to re-search.\n\n" +
|
|
696
|
-
"`body_markdown` is converted server-side to styled HTML with inlined CSS for email-client compatibility. Write the body in plain Markdown — do not pre-render HTML.\n\n" +
|
|
697
|
-
"`attachment_paths` takes absolute file paths on the user's local disk and uploads them with the draft. Useful right after search_and_fetch_emails downloaded attachments — those local paths can be passed directly here.",
|
|
698
|
-
inputSchema: {
|
|
699
|
-
body_markdown: z.string(),
|
|
700
|
-
reply_to_email_id: z.string().optional(),
|
|
701
|
-
subject: z.string().optional(),
|
|
702
|
-
to_recipients: z.array(z.string()).optional(),
|
|
703
|
-
attachment_paths: z.array(z.string()).optional(),
|
|
704
|
-
mailbox_address: z
|
|
705
|
-
.string()
|
|
706
|
-
.optional()
|
|
707
|
-
.describe(
|
|
708
|
-
"Optional target mailbox email address to create the draft in.",
|
|
709
|
-
),
|
|
778
|
+
server.registerTool(
|
|
779
|
+
"logout_of_adeu_cloud",
|
|
780
|
+
{ description: "Logs out of the Adeu Cloud backend." },
|
|
781
|
+
async () => {
|
|
782
|
+
try {
|
|
783
|
+
return (await logout_of_adeu_cloud()) as any;
|
|
784
|
+
} catch (e: any) {
|
|
785
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
786
|
+
}
|
|
710
787
|
},
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
788
|
+
);
|
|
789
|
+
server.registerTool(
|
|
790
|
+
"create_email_draft",
|
|
791
|
+
{
|
|
792
|
+
description:
|
|
793
|
+
"Creates an email draft in the user's native draft box (Outlook Drafts or Gmail Drafts).\n\n" +
|
|
794
|
+
"TWO MODES:\n" +
|
|
795
|
+
"1. Reply mode: pass `reply_to_email_id` to create a threaded reply. The draft inherits subject, recipients, and threading headers from the original — do NOT pass `subject` or `to_recipients`.\n" +
|
|
796
|
+
"2. New email mode: omit `reply_to_email_id` and pass BOTH `subject` and `to_recipients`.\n\n" +
|
|
797
|
+
"`reply_to_email_id` accepts the same ID formats as search_and_fetch_emails (`msg_*` short IDs, `adeu_*` references, or raw provider IDs). Short IDs are validated against the local cache before the call; stale ones fail fast with a clear error telling you to re-search.\n\n" +
|
|
798
|
+
"`body_markdown` is converted server-side to styled HTML with inlined CSS for email-client compatibility. Write the body in plain Markdown — do not pre-render HTML.\n\n" +
|
|
799
|
+
"`attachment_paths` takes absolute file paths on the user's local disk and uploads them with the draft. Useful right after search_and_fetch_emails downloaded attachments — those local paths can be passed directly here.",
|
|
800
|
+
inputSchema: {
|
|
801
|
+
body_markdown: z.string(),
|
|
802
|
+
reply_to_email_id: z.string().optional(),
|
|
803
|
+
subject: z.string().optional(),
|
|
804
|
+
to_recipients: z.array(z.string()).optional(),
|
|
805
|
+
attachment_paths: z.array(z.string()).optional(),
|
|
806
|
+
mailbox_address: z
|
|
807
|
+
.string()
|
|
808
|
+
.optional()
|
|
809
|
+
.describe(
|
|
810
|
+
"Optional target mailbox email address to create the draft in.",
|
|
811
|
+
),
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
async (args) => {
|
|
815
|
+
try {
|
|
816
|
+
return (await create_email_draft(args)) as any;
|
|
817
|
+
} catch (e: any) {
|
|
818
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
);
|
|
822
|
+
server.registerTool(
|
|
823
|
+
"list_available_mailboxes",
|
|
824
|
+
{
|
|
825
|
+
description:
|
|
826
|
+
"Lists all personal and shared/delegated mailboxes the authenticated Adeu user has access to, across ALL of their linked provider accounts. Returns each mailbox's `email_address`, `display_name`, auto-processing settings, and write-back preference.\n\n" +
|
|
827
|
+
"This is the right tool to answer 'which accounts/mailboxes am I logged into?' — Adeu login is user-level, so a single MCP session can see every mailbox listed here regardless of which provider account was used for SSO.\n\n" +
|
|
828
|
+
"Call this FIRST when the user names a specific mailbox or shared inbox, to resolve the canonical `email_address`. Then pass that address as `mailbox_address` to `search_and_fetch_emails` or `create_email_draft` to scope the operation. Omitting `mailbox_address` on those tools targets the user's primary personal mailbox.",
|
|
829
|
+
inputSchema: {},
|
|
830
|
+
},
|
|
831
|
+
async () => {
|
|
832
|
+
try {
|
|
833
|
+
return (await list_available_mailboxes()) as any;
|
|
834
|
+
} catch (e: any) {
|
|
835
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
);
|
|
737
839
|
}
|
|
738
840
|
|
|
739
841
|
// --- Formatter for process_document_batch ---
|
|
@@ -748,8 +850,17 @@ export function formatBatchResult(
|
|
|
748
850
|
} else {
|
|
749
851
|
res = `Batch complete. Saved to: ${outPath}\n`;
|
|
750
852
|
}
|
|
751
|
-
const total_occurrences = stats.edits
|
|
752
|
-
|
|
853
|
+
const total_occurrences = stats.edits
|
|
854
|
+
? stats.edits.reduce(
|
|
855
|
+
(acc: number, e: any) =>
|
|
856
|
+
acc + (e.status === "applied" ? e.occurrences_modified || 1 : 0),
|
|
857
|
+
0,
|
|
858
|
+
)
|
|
859
|
+
: 0;
|
|
860
|
+
const occ_text =
|
|
861
|
+
total_occurrences > stats.edits_applied
|
|
862
|
+
? ` (${total_occurrences} occurrences)`
|
|
863
|
+
: "";
|
|
753
864
|
|
|
754
865
|
res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.\n`;
|
|
755
866
|
res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.\n`;
|
|
@@ -760,20 +871,22 @@ export function formatBatchResult(
|
|
|
760
871
|
const report = stats.edits[i];
|
|
761
872
|
const status_indicator =
|
|
762
873
|
report.status === "applied" ? "✅ [applied]" : "❌ [failed]";
|
|
763
|
-
|
|
764
|
-
const pagesStr =
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
874
|
+
|
|
875
|
+
const pagesStr =
|
|
876
|
+
report.pages && report.pages.length > 0
|
|
877
|
+
? ` (p${report.pages.join(", p")})`
|
|
878
|
+
: "";
|
|
879
|
+
|
|
768
880
|
res += `### Edit ${i + 1} ${status_indicator}${pagesStr}\n`;
|
|
769
|
-
|
|
881
|
+
|
|
770
882
|
if (report.heading_path) {
|
|
771
883
|
res += `**Path:** \`${report.heading_path}\`\n`;
|
|
772
884
|
}
|
|
773
|
-
|
|
885
|
+
|
|
774
886
|
if (report.match_mode) {
|
|
775
|
-
|
|
776
|
-
|
|
887
|
+
const occ =
|
|
888
|
+
report.occurrences_modified || (report.status === "applied" ? 1 : 0);
|
|
889
|
+
res += `**Mode:** \`${report.match_mode}\` (${occ} occurrence${occ !== 1 ? "s" : ""} modified)\n`;
|
|
777
890
|
}
|
|
778
891
|
|
|
779
892
|
if (report.error) {
|
|
@@ -782,12 +895,12 @@ export function formatBatchResult(
|
|
|
782
895
|
if (report.warning) {
|
|
783
896
|
res += `*Warning:* ${report.warning}\n`;
|
|
784
897
|
}
|
|
785
|
-
|
|
898
|
+
|
|
786
899
|
if (report.critic_markup) {
|
|
787
|
-
res += `*Preview (CriticMarkup):*\n> ${report.critic_markup.split(
|
|
900
|
+
res += `*Preview (CriticMarkup):*\n> ${report.critic_markup.split("\\n").join("\\n> ")}\n`;
|
|
788
901
|
}
|
|
789
902
|
if (report.clean_text) {
|
|
790
|
-
res += `*Preview (Clean):*\n> ${report.clean_text.split(
|
|
903
|
+
res += `*Preview (Clean):*\n> ${report.clean_text.split("\\n").join("\\n> ")}\n`;
|
|
791
904
|
}
|
|
792
905
|
res += "\n";
|
|
793
906
|
}
|