@adeu/mcp-server 1.18.1 → 1.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -204,12 +204,17 @@ function build_search_response(text, search_query, search_regex, search_case_sen
204
204
  const [body] = split_structural_appendix(text);
205
205
  const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
206
206
  const flags = search_case_sensitive ? "g" : "gi";
207
- const patternStr = search_regex ? search_query : escapeRegExp(search_query);
207
+ let regexDowngradedNote = "";
208
208
  let regex;
209
- try {
210
- regex = new RegExp(patternStr, flags);
211
- } catch (e) {
212
- throw new Error(`Invalid regex pattern: ${e.message}`);
209
+ if (search_regex) {
210
+ try {
211
+ regex = new RegExp(search_query, flags);
212
+ } catch (e) {
213
+ regexDowngradedNote = `> **Note:** \`${search_query}\` is not a valid regular expression (${e.message}), so it was searched as literal text instead. If you meant a regex, fix the pattern; if you meant literal text, set \`search_regex\` to false.`;
214
+ regex = new RegExp(escapeRegExp(search_query), flags);
215
+ }
216
+ } else {
217
+ regex = new RegExp(escapeRegExp(search_query), flags);
213
218
  }
214
219
  const allMatches = Array.from(body.matchAll(regex));
215
220
  const pag_res = paginate(body, "");
@@ -264,6 +269,9 @@ The query DOES appear elsewhere in the document (${allMatches.length} match${all
264
269
 
265
270
  Verify your search spelling, or try setting \`search_case_sensitive\` to false or enabling \`search_regex\` if you used pattern wildcards.`;
266
271
  }
272
+ if (regexDowngradedNote) body_msg = `${regexDowngradedNote}
273
+
274
+ ${body_msg}`;
267
275
  const llm_content2 = `> **File Path:** \`${resolve(file_path)}\`
268
276
 
269
277
  ${body_msg}`;
@@ -354,6 +362,7 @@ ${body_msg}`;
354
362
  );
355
363
  i++;
356
364
  }
365
+ if (regexDowngradedNote) ui_parts.unshift(regexDowngradedNote);
357
366
  const ui_markdown = ui_parts.join("\n\n");
358
367
  const llm_content = `> **File Path:** \`${resolve(file_path)}\`
359
368
 
@@ -1220,6 +1229,36 @@ async function list_available_mailboxes() {
1220
1229
  }
1221
1230
 
1222
1231
  // src/index.ts
1232
+ var MATCH_MODE_SYNONYMS = {
1233
+ strict: "strict",
1234
+ first: "first",
1235
+ all: "all",
1236
+ first_only: "first",
1237
+ firstonly: "first",
1238
+ "first-only": "first",
1239
+ all_occurrences: "all",
1240
+ alloccurrences: "all",
1241
+ "all-occurrences": "all",
1242
+ every: "all"
1243
+ };
1244
+ function coerceChangeItemInPlace(item) {
1245
+ if (item === null || typeof item !== "object" || Array.isArray(item)) return;
1246
+ if (!("type" in item) || item.type === void 0 || item.type === null) {
1247
+ if ("cells" in item) item.type = "insert_row";
1248
+ else if ("text" in item && "target_id" in item) item.type = "reply";
1249
+ else if ("target_text" in item && "new_text" in item) item.type = "modify";
1250
+ }
1251
+ if ("match_mode" in item) {
1252
+ const raw = item.match_mode;
1253
+ if (typeof raw !== "string") {
1254
+ delete item.match_mode;
1255
+ } else {
1256
+ const mapped = MATCH_MODE_SYNONYMS[raw.trim().toLowerCase()];
1257
+ if (mapped === void 0) delete item.match_mode;
1258
+ else item.match_mode = mapped;
1259
+ }
1260
+ }
1261
+ }
1223
1262
  function readFileBytesOrThrow(filePath) {
1224
1263
  try {
1225
1264
  return readFileSync3(filePath);
@@ -1250,8 +1289,8 @@ var READ_DOCX_TAIL = "Modes:\n- 'full' (default): paginated body content. Use pa
1250
1289
  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";
1251
1290
  var PROCESS_BATCH_OPERATIONS_DESC = "Each item in `changes` must specify a `type`:\n1. 'modify': Search-and-replace. By default `target_text` must match uniquely (`match_mode`:'strict') \u2014 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\u2026). `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.\n \u2022 EMPTY/FORM TABLE CELLS: a blank cell has no text to match. `read_docx` renders each cell with a trailing `{#cell:<id>}` anchor \u2014 to fill a blank cell, set `target_text` to that exact anchor (e.g. '{#cell:0000005E}') and put the value in `new_text`. Do NOT try to match the pipe layout ('Date | | |'); the pipes are display separators, not editable text.\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. The `{#cell:<id>}` anchors are stable (Word-assigned) and safe to reuse across reads.\n\n`author_name` is used for attribution on all tracked changes and comments, in both disk and Live Word modes.";
1252
1291
  var DIFF_DOCX_DESC = "Compares two DOCX files and returns a compact `@@ Word Patch @@` diff \u2014 Adeu's token-level, sub-word patch format \u2014 of their text content. Useful for analyzing differences between versions before editing.";
1253
- var gitSha = "988af3a";
1254
- var packageVersion = "1.18.1";
1292
+ var gitSha = "dc59148";
1293
+ var packageVersion = "1.18.4";
1255
1294
  var buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
1256
1295
  var args = process.argv.slice(2);
1257
1296
  var scopeIdx = args.indexOf("--scope");
@@ -1347,6 +1386,9 @@ registerAppTool(
1347
1386
  title: "Read DOCX",
1348
1387
  description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,
1349
1388
  inputSchema: z.object({
1389
+ reasoning: z.string().describe(
1390
+ "Why do I need to read this docx document? State this reason before any other parameter."
1391
+ ),
1350
1392
  file_path: z.string().describe("Absolute path to the DOCX file."),
1351
1393
  clean_view: z.boolean().default(false).describe(
1352
1394
  "If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text."
@@ -1370,6 +1412,7 @@ registerAppTool(
1370
1412
  _meta: { ui: { resourceUri: MARKDOWN_UI_URI } }
1371
1413
  },
1372
1414
  async ({
1415
+ reasoning,
1373
1416
  file_path,
1374
1417
  clean_view,
1375
1418
  mode,
@@ -1381,6 +1424,7 @@ registerAppTool(
1381
1424
  search_case_sensitive
1382
1425
  }) => {
1383
1426
  try {
1427
+ void reasoning;
1384
1428
  const buf = readFileBytesOrThrow(file_path);
1385
1429
  if (mode === "outline") {
1386
1430
  const doc = await DocumentObject2.load(buf);
@@ -1433,8 +1477,8 @@ registerAppTool(
1433
1477
  }
1434
1478
  );
1435
1479
  var CHANGE_ITEM_SCHEMA = z.object({
1436
- type: z.enum(["modify", "accept", "reject", "reply", "insert_row", "delete_row"]).describe(
1437
- "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)."
1480
+ type: z.enum(["modify", "accept", "reject", "reply", "insert_row", "delete_row"]).optional().describe(
1481
+ "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). If omitted it is inferred when unambiguous from the other fields."
1438
1482
  ),
1439
1483
  target_text: z.string().optional().describe(
1440
1484
  "modify / insert_row / delete_row: the existing text to locate (interpreted as a regex when regex=true)."
@@ -1465,6 +1509,9 @@ server.registerTool(
1465
1509
  {
1466
1510
  description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,
1467
1511
  inputSchema: {
1512
+ reasoning: z.string().describe(
1513
+ "Why do I need to apply these changes to the document? State this reason before any other parameter."
1514
+ ),
1468
1515
  original_docx_path: z.string().describe("Absolute path to the source file."),
1469
1516
  author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
1470
1517
  changes: z.array(z.union([z.string(), CHANGE_ITEM_SCHEMA])).describe(
@@ -1477,6 +1524,7 @@ server.registerTool(
1477
1524
  }
1478
1525
  },
1479
1526
  async ({
1527
+ reasoning,
1480
1528
  original_docx_path,
1481
1529
  author_name,
1482
1530
  changes,
@@ -1484,6 +1532,7 @@ server.registerTool(
1484
1532
  dry_run
1485
1533
  }) => {
1486
1534
  try {
1535
+ void reasoning;
1487
1536
  if (!author_name || !author_name.trim())
1488
1537
  return {
1489
1538
  content: [
@@ -1495,19 +1544,49 @@ server.registerTool(
1495
1544
  content: [{ type: "text", text: "Error: No changes provided." }]
1496
1545
  };
1497
1546
  const sanitizedChanges = changes.map((item) => {
1547
+ let obj = item;
1498
1548
  if (typeof item === "string") {
1499
1549
  try {
1500
1550
  const parsed = JSON.parse(item);
1501
- if (parsed !== null && typeof parsed === "object") {
1502
- return parsed;
1503
- }
1504
- return item;
1551
+ obj = parsed !== null && typeof parsed === "object" ? parsed : item;
1505
1552
  } catch {
1506
- return item;
1553
+ obj = item;
1507
1554
  }
1508
1555
  }
1509
- return item;
1556
+ if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
1557
+ coerceChangeItemInPlace(obj);
1558
+ }
1559
+ return obj;
1510
1560
  });
1561
+ const VALID_TYPES = /* @__PURE__ */ new Set([
1562
+ "modify",
1563
+ "accept",
1564
+ "reject",
1565
+ "reply",
1566
+ "insert_row",
1567
+ "delete_row"
1568
+ ]);
1569
+ const typeErrors = [];
1570
+ sanitizedChanges.forEach((c, i) => {
1571
+ if (c !== null && typeof c === "object" && !Array.isArray(c) && (!c.type || !VALID_TYPES.has(c.type))) {
1572
+ typeErrors.push(
1573
+ `- Change ${i + 1}: missing or unrecognized "type". Use one of: modify (needs target_text + new_text), accept/reject (needs target_id like "Chg:12"), reply (needs target_id like "Com:5" + text), insert_row (needs target_text + cells), delete_row (needs target_text). Received keys: [${Object.keys(c).join(", ")}].`
1574
+ );
1575
+ }
1576
+ });
1577
+ if (typeErrors.length > 0) {
1578
+ return {
1579
+ isError: true,
1580
+ content: [
1581
+ {
1582
+ type: "text",
1583
+ text: `Batch rejected. Some changes are malformed:
1584
+
1585
+ ${typeErrors.join("\n")}`
1586
+ }
1587
+ ]
1588
+ };
1589
+ }
1511
1590
  let outPath = output_path;
1512
1591
  if (!outPath) {
1513
1592
  const ext = extname(original_docx_path);
@@ -1560,12 +1639,16 @@ server.registerTool(
1560
1639
  {
1561
1640
  description: "Accepts all tracked changes and removes all comments in a single operation.",
1562
1641
  inputSchema: {
1642
+ reasoning: z.string().describe(
1643
+ "Why do I need to accept all changes in this document? State this reason before any other parameter."
1644
+ ),
1563
1645
  docx_path: z.string().describe("Absolute path to the DOCX file."),
1564
1646
  output_path: z.string().optional().describe("Optional output path.")
1565
1647
  }
1566
1648
  },
1567
- async ({ docx_path, output_path }) => {
1649
+ async ({ reasoning, docx_path, output_path }) => {
1568
1650
  try {
1651
+ void reasoning;
1569
1652
  let outPath = output_path;
1570
1653
  if (!outPath) {
1571
1654
  const ext = extname(docx_path);
@@ -1597,6 +1680,9 @@ server.registerTool(
1597
1680
  {
1598
1681
  description: DIFF_DOCX_DESC,
1599
1682
  inputSchema: {
1683
+ reasoning: z.string().describe(
1684
+ "Why do I need to diff these two documents? State this reason before any other parameter."
1685
+ ),
1600
1686
  original_path: z.string().describe("Absolute path to the baseline DOCX file."),
1601
1687
  modified_path: z.string().describe("Absolute path to the modified DOCX file."),
1602
1688
  compare_clean: z.boolean().default(true).describe(
@@ -1604,8 +1690,9 @@ server.registerTool(
1604
1690
  )
1605
1691
  }
1606
1692
  },
1607
- async ({ original_path, modified_path, compare_clean }) => {
1693
+ async ({ reasoning, original_path, modified_path, compare_clean }) => {
1608
1694
  try {
1695
+ void reasoning;
1609
1696
  const origBuf = readFileBytesOrThrow(original_path);
1610
1697
  const modBuf = readFileBytesOrThrow(modified_path);
1611
1698
  const origText = await extractTextFromBuffer(origBuf, compare_clean);
@@ -1632,6 +1719,9 @@ server.registerTool(
1632
1719
  {
1633
1720
  description: "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.",
1634
1721
  inputSchema: {
1722
+ reasoning: z.string().describe(
1723
+ "Why do I need to finalize this document? State this reason before any other parameter."
1724
+ ),
1635
1725
  file_path: z.string().describe("Absolute path to the DOCX file."),
1636
1726
  output_path: z.string().optional().describe("Optional output path."),
1637
1727
  sanitize_mode: z.enum(["full", "keep-markup"]).optional().describe("full removes all markup, keep-markup redacts metadata."),
@@ -1647,6 +1737,7 @@ server.registerTool(
1647
1737
  }
1648
1738
  },
1649
1739
  async ({
1740
+ reasoning,
1650
1741
  file_path,
1651
1742
  output_path,
1652
1743
  sanitize_mode,
@@ -1656,6 +1747,7 @@ server.registerTool(
1656
1747
  export_pdf
1657
1748
  }) => {
1658
1749
  try {
1750
+ void reasoning;
1659
1751
  let outPath = output_path;
1660
1752
  if (!outPath) {
1661
1753
  const ext = extname(file_path);
@@ -1700,6 +1792,9 @@ if (!isDocxOnly) {
1700
1792
  title: "Search & Fetch Emails",
1701
1793
  description: "Searches the user's live email inbox via the Adeu cloud backend.\n\nTWO MODES:\n1. 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.\n2. 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\nAUTO-ESCALATION: If a search returns exactly one preview, the backend automatically fetches the full email in the same call. Plan around the response shape \u2014 check the `type` field (`previews` vs `full_email`) before assuming.\n\nEMAIL ID FORMATS (`email_id` parameter accepts any of):\n- `msg_<6 chars>` \u2014 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- `adeu_<numeric>` \u2014 server-side reference for emails Adeu has previously processed. Portable across machines and sessions for the same authenticated user.\n- Raw provider ID (Gmail/Outlook native ID) \u2014 works if you have it, but you usually won't.\n\nFOLDER 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\nATTACHMENTS: attachments larger than `max_attachment_size_mb` (default 10) are listed in the response but NOT downloaded \u2014 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.",
1702
1794
  inputSchema: z.object({
1795
+ reasoning: z.string().describe(
1796
+ "Why do I need to search or fetch these emails? State this reason before any other parameter."
1797
+ ),
1703
1798
  sender: z.string().optional(),
1704
1799
  subject: z.string().optional(),
1705
1800
  has_attachments: z.boolean().optional(),
@@ -1733,7 +1828,12 @@ if (!isDocxOnly) {
1733
1828
  server.registerTool(
1734
1829
  "login_to_adeu_cloud",
1735
1830
  {
1736
- description: "Logs the user into Adeu Cloud. Opens a browser window for SSO authentication.\n\nIMPORTANT \u2014 login is user-level, not account-level:\n- An Adeu user can have multiple linked provider accounts (Microsoft, Google) and multiple mailboxes (personal + shared/delegated). One linked account is marked primary.\n- 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 \u2014 not just the one used to sign in.\n- 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\nWhen the user asks which accounts or mailboxes are available, call `list_available_mailboxes` rather than naming a single account from the login response."
1831
+ description: "Logs the user into Adeu Cloud. Opens a browser window for SSO authentication.\n\nIMPORTANT \u2014 login is user-level, not account-level:\n- An Adeu user can have multiple linked provider accounts (Microsoft, Google) and multiple mailboxes (personal + shared/delegated). One linked account is marked primary.\n- 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 \u2014 not just the one used to sign in.\n- 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\nWhen the user asks which accounts or mailboxes are available, call `list_available_mailboxes` rather than naming a single account from the login response.",
1832
+ inputSchema: {
1833
+ reasoning: z.string().describe(
1834
+ "Why do I need to log in to Adeu Cloud? State this reason before any other parameter."
1835
+ )
1836
+ }
1737
1837
  },
1738
1838
  async () => {
1739
1839
  try {
@@ -1745,7 +1845,14 @@ if (!isDocxOnly) {
1745
1845
  );
1746
1846
  server.registerTool(
1747
1847
  "logout_of_adeu_cloud",
1748
- { description: "Logs out of the Adeu Cloud backend." },
1848
+ {
1849
+ description: "Logs out of the Adeu Cloud backend.",
1850
+ inputSchema: {
1851
+ reasoning: z.string().describe(
1852
+ "Why do I need to log out of Adeu Cloud? State this reason before any other parameter."
1853
+ )
1854
+ }
1855
+ },
1749
1856
  async () => {
1750
1857
  try {
1751
1858
  return await logout_of_adeu_cloud();
@@ -1759,6 +1866,9 @@ if (!isDocxOnly) {
1759
1866
  {
1760
1867
  description: "Creates an email draft in the user's native draft box (Outlook Drafts or Gmail Drafts).\n\nTWO MODES:\n1. Reply mode: pass `reply_to_email_id` to create a threaded reply. The draft inherits subject, recipients, and threading headers from the original \u2014 do NOT pass `subject` or `to_recipients`.\n2. New email mode: omit `reply_to_email_id` and pass BOTH `subject` and `to_recipients`.\n\n`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`body_markdown` is converted server-side to styled HTML with inlined CSS for email-client compatibility. Write the body in plain Markdown \u2014 do not pre-render HTML.\n\n`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 \u2014 those local paths can be passed directly here.",
1761
1868
  inputSchema: {
1869
+ reasoning: z.string().describe(
1870
+ "Why do I need to create this email draft? State this reason before any other parameter."
1871
+ ),
1762
1872
  body_markdown: z.string(),
1763
1873
  reply_to_email_id: z.string().optional(),
1764
1874
  subject: z.string().optional(),
@@ -1781,7 +1891,11 @@ if (!isDocxOnly) {
1781
1891
  "list_available_mailboxes",
1782
1892
  {
1783
1893
  description: "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\nThis is the right tool to answer 'which accounts/mailboxes am I logged into?' \u2014 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\nCall 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.",
1784
- inputSchema: {}
1894
+ inputSchema: {
1895
+ reasoning: z.string().describe(
1896
+ "Why do I need to list available mailboxes? State this reason before any other parameter."
1897
+ )
1898
+ }
1785
1899
  },
1786
1900
  async () => {
1787
1901
  try {
@@ -1857,8 +1971,8 @@ ${stats.skipped_details.join("\n")}`;
1857
1971
  async function main() {
1858
1972
  const transport = new StdioServerTransport();
1859
1973
  await server.connect(transport);
1860
- const gitSha2 = "988af3a";
1861
- const buildTs = "2026-06-30T18:57:29.292Z";
1974
+ const gitSha2 = "dc59148";
1975
+ const buildTs = "2026-07-05T06:17:14.425Z";
1862
1976
  console.error(
1863
1977
  `Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha2}@${buildTs}`
1864
1978
  );