@adeu/mcp-server 1.13.0 → 1.15.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 CHANGED
@@ -200,6 +200,128 @@ ${ui_markdown}`;
200
200
  }
201
201
  };
202
202
  }
203
+ function build_search_response(text, search_query, search_regex, search_case_sensitive, page, file_path) {
204
+ const [body] = split_structural_appendix(text);
205
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
206
+ const flags = search_case_sensitive ? "g" : "gi";
207
+ const patternStr = search_regex ? search_query : escapeRegExp(search_query);
208
+ let regex;
209
+ try {
210
+ regex = new RegExp(patternStr, flags);
211
+ } catch (e) {
212
+ throw new Error(`Invalid regex pattern: ${e.message}`);
213
+ }
214
+ const matches = Array.from(body.matchAll(regex));
215
+ if (matches.length === 0) {
216
+ const ui_markdown2 = `> **Search Results** \u2014 No matches found for query \`${search_query}\` in \`${basename(file_path)}\`.
217
+
218
+ Verify your search spelling, or try setting \`search_case_sensitive\` to false or enabling \`search_regex\` if you used pattern wildcards.`;
219
+ const llm_content2 = `> **File Path:** \`${resolve(file_path)}\`
220
+
221
+ ${ui_markdown2}`;
222
+ return {
223
+ content: [{ type: "text", text: llm_content2 }],
224
+ structuredContent: {
225
+ markdown: ui_markdown2,
226
+ title: `Search: ${basename(file_path)}`,
227
+ file_path: resolve(file_path)
228
+ }
229
+ };
230
+ }
231
+ const pag_res = paginate(body, "");
232
+ const page_offsets = pag_res.body_page_offsets;
233
+ const total_matches = matches.length;
234
+ const total_pages = Math.ceil(total_matches / 10);
235
+ let start_idx = 0;
236
+ let end_idx = total_matches;
237
+ let page_text = "all";
238
+ const pageStr = String(page).toLowerCase();
239
+ if (pageStr !== "all") {
240
+ const page_num = parseInt(pageStr, 10);
241
+ if (isNaN(page_num) || page_num < 1 || page_num > total_pages) {
242
+ throw new Error(`Page ${page} out of range (search has ${total_pages} pages).`);
243
+ }
244
+ start_idx = (page_num - 1) * 10;
245
+ end_idx = Math.min(start_idx + 10, total_matches);
246
+ page_text = `${page_num} of ${total_pages}`;
247
+ }
248
+ const page_matches = matches.slice(start_idx, end_idx);
249
+ const ui_parts = [
250
+ `> **Search Results** \u2014 Found ${total_matches} matches for query \`${search_query}\` in \`${basename(file_path)}\`.`
251
+ ];
252
+ if (total_pages > 1 && pageStr !== "all") {
253
+ const nextPage = parseInt(pageStr, 10) + 1;
254
+ ui_parts.push(`> Showing page ${page_text} (matches ${start_idx + 1}-${end_idx}). To see more matches, call \`read_docx\` with \`search_query='${search_query}'\`, \`search_regex=${search_regex ? "true" : "false"}\`, and \`page=${nextPage}\`.`);
255
+ }
256
+ const occurrences_map = {};
257
+ for (const m of matches) {
258
+ const matched_str = m[0];
259
+ occurrences_map[matched_str] = (occurrences_map[matched_str] || 0) + 1;
260
+ }
261
+ function get_heading(idx, txt) {
262
+ const txtBefore = txt.substring(0, idx);
263
+ const lines = txtBefore.split("\n");
264
+ const path = [];
265
+ let current_level = 999;
266
+ for (let i2 = lines.length - 1; i2 >= 0; i2--) {
267
+ const line = lines[i2];
268
+ const m = line.match(/^(#{1,6})\s+(.*)/);
269
+ if (m) {
270
+ const level = m[1].length;
271
+ if (level < current_level) {
272
+ let cleanHeading = m[2].replace(/\*\*|__|[*_]/g, "").replace(/\{#[^}]+\}/g, "").trim();
273
+ if (cleanHeading.length > 80) {
274
+ cleanHeading = cleanHeading.substring(0, 80) + "...";
275
+ }
276
+ path.unshift(cleanHeading);
277
+ current_level = level;
278
+ if (level === 1) break;
279
+ }
280
+ }
281
+ }
282
+ return path.join(" > ");
283
+ }
284
+ let i = start_idx + 1;
285
+ for (const m of page_matches) {
286
+ const matched_str = m[0];
287
+ const m_start = m.index;
288
+ const m_end = m_start + matched_str.length;
289
+ let p_num = 1;
290
+ for (let j = 0; j < page_offsets.length; j++) {
291
+ if (m_start >= page_offsets[j]) {
292
+ p_num = j + 1;
293
+ } else {
294
+ break;
295
+ }
296
+ }
297
+ const snippet_start = Math.max(0, m_start - 100);
298
+ const snippet_end = Math.min(body.length, m_end + 100);
299
+ const snippet = body.substring(snippet_start, m_start) + `**${matched_str}**` + body.substring(m_end, snippet_end);
300
+ const snippet_lines = snippet.split("\n").filter((line) => line.trim().length > 0).map((line) => `> ${line}`).join("\n");
301
+ ui_parts.push("---");
302
+ ui_parts.push(`### Match ${i} (p${p_num})`);
303
+ const h_path = get_heading(m_start, body);
304
+ if (h_path) {
305
+ ui_parts.push(`**Path:** \`${h_path}\``);
306
+ }
307
+ const count = occurrences_map[matched_str];
308
+ ui_parts.push(snippet_lines);
309
+ ui_parts.push(`*Occurrences:* This exact phrasing appears ${count} time${count !== 1 ? "s" : ""} in the document.`);
310
+ i++;
311
+ }
312
+ const ui_markdown = ui_parts.join("\n\n");
313
+ const llm_content = `> **File Path:** \`${resolve(file_path)}\`
314
+
315
+ ${ui_markdown}`;
316
+ return {
317
+ content: [{ type: "text", text: llm_content }],
318
+ structuredContent: {
319
+ markdown: ui_markdown,
320
+ title: `Search: ${basename(file_path)}`,
321
+ file_path: resolve(file_path)
322
+ }
323
+ };
324
+ }
203
325
 
204
326
  // src/desktop-auth.ts
205
327
  import { createServer } from "http";
@@ -1089,8 +1211,8 @@ var READ_DOCX_TAIL = "Modes:\n- 'full' (default): paginated body content. Use pa
1089
1211
  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";
1090
1212
  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.";
1091
1213
  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.";
1092
- var gitSha = "cab9a8d";
1093
- var packageVersion = "1.13.0";
1214
+ var gitSha = "fa47c47";
1215
+ var packageVersion = "1.15.0";
1094
1216
  var buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
1095
1217
  var args = process.argv.slice(2);
1096
1218
  var scopeIdx = args.indexOf("--scope");
@@ -1193,9 +1315,12 @@ registerAppTool(
1193
1315
  mode: z.enum(["full", "outline", "appendix"]).default("full").describe(
1194
1316
  "'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms."
1195
1317
  ),
1196
- page: z.number().default(1).describe("Page number (1-indexed) for mode='full'. Defaults to 1."),
1318
+ page: z.union([z.number(), z.literal("all")]).default(1).describe("Page number (1-indexed) for mode='full', or 'all' to return unpaginated."),
1197
1319
  outline_max_level: z.number().default(2).describe("For mode='outline' only: cap on heading depth."),
1198
- outline_verbose: z.boolean().default(false).describe("For mode='outline' only: includes metadata.")
1320
+ outline_verbose: z.boolean().default(false).describe("For mode='outline' only: includes metadata."),
1321
+ search_query: z.string().optional().describe("The substring or regex pattern to search for. When provided, filters results to matching paragraphs."),
1322
+ search_regex: z.boolean().default(false).describe("Set to true to interpret search_query as a regular expression."),
1323
+ search_case_sensitive: z.boolean().default(true).describe("Set to false to perform case-insensitive matching.")
1199
1324
  }),
1200
1325
  _meta: { ui: { resourceUri: MARKDOWN_UI_URI } }
1201
1326
  },
@@ -1205,7 +1330,10 @@ registerAppTool(
1205
1330
  mode,
1206
1331
  page,
1207
1332
  outline_max_level,
1208
- outline_verbose
1333
+ outline_verbose,
1334
+ search_query,
1335
+ search_regex,
1336
+ search_case_sensitive
1209
1337
  }) => {
1210
1338
  try {
1211
1339
  const buf = readFileBytesOrThrow(file_path);
@@ -1223,11 +1351,15 @@ registerAppTool(
1223
1351
  return res2;
1224
1352
  }
1225
1353
  const text = await extractTextFromBuffer(buf, clean_view);
1354
+ if (search_query !== void 0 && search_query !== null) {
1355
+ const res2 = build_search_response(text, search_query, search_regex, search_case_sensitive, page, file_path);
1356
+ return res2;
1357
+ }
1226
1358
  if (mode === "appendix") {
1227
- const res2 = build_appendix_response(text, page, file_path);
1359
+ const res2 = build_appendix_response(text, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
1228
1360
  return res2;
1229
1361
  }
1230
- const res = build_paginated_response(text, page, file_path);
1362
+ const res = build_paginated_response(text, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
1231
1363
  return res;
1232
1364
  } catch (e) {
1233
1365
  return {
@@ -1274,6 +1406,20 @@ server.registerTool(
1274
1406
  return {
1275
1407
  content: [{ type: "text", text: "Error: No changes provided." }]
1276
1408
  };
1409
+ const sanitizedChanges = changes.map((item) => {
1410
+ if (typeof item === "string") {
1411
+ try {
1412
+ const parsed = JSON.parse(item);
1413
+ if (parsed !== null && typeof parsed === "object") {
1414
+ return parsed;
1415
+ }
1416
+ return item;
1417
+ } catch {
1418
+ return item;
1419
+ }
1420
+ }
1421
+ return item;
1422
+ });
1277
1423
  let outPath = output_path;
1278
1424
  if (!outPath) {
1279
1425
  const ext = extname(original_docx_path);
@@ -1286,7 +1432,7 @@ server.registerTool(
1286
1432
  const engine = new RedlineEngine(doc, author_name);
1287
1433
  let stats;
1288
1434
  try {
1289
- stats = engine.process_batch(changes, dry_run);
1435
+ stats = engine.process_batch(sanitizedChanges, dry_run);
1290
1436
  } catch (e) {
1291
1437
  if (e instanceof BatchValidationError) {
1292
1438
  return {
@@ -1561,52 +1707,61 @@ function formatBatchResult(stats, outPath, dry_run) {
1561
1707
  res = `Batch complete. Saved to: ${outPath}
1562
1708
  `;
1563
1709
  }
1710
+ const total_occurrences = stats.edits ? stats.edits.reduce((acc, e) => acc + (e.status === "applied" ? e.occurrences_modified || 1 : 0), 0) : 0;
1711
+ const occ_text = total_occurrences > stats.edits_applied ? ` (${total_occurrences} occurrences)` : "";
1564
1712
  res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
1565
1713
  `;
1566
- res += `Edits: ${stats.edits_applied} applied, ${stats.edits_skipped} skipped.
1714
+ res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.
1567
1715
  `;
1568
1716
  if (stats.edits && stats.edits.length > 0) {
1569
1717
  res += "\nDetailed Edit Reports:\n";
1570
1718
  for (let i = 0; i < stats.edits.length; i++) {
1571
1719
  const report = stats.edits[i];
1572
1720
  const status_indicator = report.status === "applied" ? "\u2705 [applied]" : "\u274C [failed]";
1573
- res += `Edit ${i + 1} ${status_indicator}:
1574
- `;
1575
- res += ` Target: '${report.target_text}'
1721
+ const pagesStr = report.pages && report.pages.length > 0 ? ` (p${report.pages.join(", p")})` : "";
1722
+ res += `### Edit ${i + 1} ${status_indicator}${pagesStr}
1576
1723
  `;
1577
- res += ` New text: '${report.new_text}'
1724
+ if (report.heading_path) {
1725
+ res += `**Path:** \`${report.heading_path}\`
1578
1726
  `;
1579
- if (report.warning) {
1580
- res += ` Warning: ${report.warning}
1727
+ }
1728
+ if (report.match_mode) {
1729
+ const occ = report.occurrences_modified || (report.status === "applied" ? 1 : 0);
1730
+ res += `**Mode:** \`${report.match_mode}\` (${occ} occurrence${occ !== 1 ? "s" : ""} modified)
1581
1731
  `;
1582
1732
  }
1583
1733
  if (report.error) {
1584
- res += ` Error: ${report.error}
1734
+ res += `*Error:* ${report.error}
1735
+ `;
1736
+ }
1737
+ if (report.warning) {
1738
+ res += `*Warning:* ${report.warning}
1585
1739
  `;
1586
1740
  }
1587
1741
  if (report.critic_markup) {
1588
- res += ` Preview (CriticMarkup): ${report.critic_markup}
1742
+ res += `*Preview (CriticMarkup):*
1743
+ > ${report.critic_markup.split("\\n").join("\\n> ")}
1589
1744
  `;
1590
1745
  }
1591
1746
  if (report.clean_text) {
1592
- res += ` Clean text preview: ${report.clean_text}
1747
+ res += `*Preview (Clean):*
1748
+ > ${report.clean_text.split("\\n").join("\\n> ")}
1593
1749
  `;
1594
1750
  }
1751
+ res += "\n";
1595
1752
  }
1596
1753
  }
1597
1754
  if (stats.skipped_details && stats.skipped_details.length > 0) {
1598
- res += `
1599
-
1600
- Skipped Details:
1755
+ res += `Skipped Details:
1601
1756
  ${stats.skipped_details.join("\n")}`;
1602
1757
  }
1603
- return res;
1758
+ return res.trim();
1604
1759
  }
1605
1760
  async function main() {
1606
1761
  const transport = new StdioServerTransport();
1607
1762
  await server.connect(transport);
1608
- const gitSha2 = "cab9a8d";
1609
- const buildTs = "2026-06-23T13:51:25.626Z";
1763
+ const gitSha2 = "fa47c47";
1764
+ const buildTs = "2026-06-25T10:39:47.565Z";
1610
1765
  console.error(
1611
1766
  `Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha2}@${buildTs}`
1612
1767
  );