@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 CHANGED
@@ -211,50 +211,100 @@ function build_search_response(text, search_query, search_regex, search_case_sen
211
211
  } catch (e) {
212
212
  throw new Error(`Invalid regex pattern: ${e.message}`);
213
213
  }
214
- const matches = Array.from(body.matchAll(regex));
214
+ const allMatches = Array.from(body.matchAll(regex));
215
+ const pag_res = paginate(body, "");
216
+ const page_offsets = pag_res.body_page_offsets;
217
+ const total_doc_pages = pag_res.total_pages;
218
+ let filter_doc_page = null;
219
+ if (page !== void 0 && page !== null) {
220
+ const pageStr = String(page).toLowerCase();
221
+ if (pageStr !== "all") {
222
+ const parsed = parseInt(pageStr, 10);
223
+ if (isNaN(parsed) || parsed < 1) {
224
+ throw new Error(
225
+ `Invalid page value: \`${page}\`. Pass a positive integer to restrict the search to that document page, omit \`page\` to search all pages, or pass \`page='all'\` explicitly.`
226
+ );
227
+ }
228
+ if (parsed > total_doc_pages) {
229
+ throw new Error(
230
+ `Document page ${parsed} is out of range \u2014 the document has ${total_doc_pages} page(s). In search mode, \`page\` filters matches to a specific document page; omit it or pass \`page='all'\` to search the whole document.`
231
+ );
232
+ }
233
+ filter_doc_page = parsed;
234
+ }
235
+ }
236
+ const pageOfOffset = (offset) => {
237
+ let p = 1;
238
+ for (let j = 0; j < page_offsets.length; j++) {
239
+ if (offset >= page_offsets[j]) p = j + 1;
240
+ else break;
241
+ }
242
+ return p;
243
+ };
244
+ const pagesWithHits = /* @__PURE__ */ new Set();
245
+ for (const m of allMatches) {
246
+ pagesWithHits.add(pageOfOffset(m.index));
247
+ }
248
+ const matches = filter_doc_page === null ? allMatches : allMatches.filter((m) => pageOfOffset(m.index) === filter_doc_page);
215
249
  if (matches.length === 0) {
216
- const ui_markdown2 = `> **Search Results** \u2014 No matches found for query \`${search_query}\` in \`${basename(file_path)}\`.
250
+ let body_msg;
251
+ if (filter_doc_page !== null) {
252
+ if (allMatches.length === 0) {
253
+ body_msg = `> **Search Results** \u2014 No matches found for query \`${search_query}\` in \`${basename(file_path)}\`.
254
+
255
+ Verify your search spelling, or try setting \`search_case_sensitive\` to false or enabling \`search_regex\` if you used pattern wildcards.`;
256
+ } else {
257
+ const hitPages = Array.from(pagesWithHits).sort((a, b) => a - b);
258
+ body_msg = `> **Search Results** \u2014 No matches for \`${search_query}\` on document page ${filter_doc_page}.
259
+
260
+ The query DOES appear elsewhere in the document (${allMatches.length} match${allMatches.length !== 1 ? "es" : ""} on page${hitPages.length !== 1 ? "s" : ""} ${hitPages.join(", ")}). Omit \`page\` or pass \`page='all'\` to see them.`;
261
+ }
262
+ } else {
263
+ body_msg = `> **Search Results** \u2014 No matches found for query \`${search_query}\` in \`${basename(file_path)}\`.
217
264
 
218
265
  Verify your search spelling, or try setting \`search_case_sensitive\` to false or enabling \`search_regex\` if you used pattern wildcards.`;
266
+ }
219
267
  const llm_content2 = `> **File Path:** \`${resolve(file_path)}\`
220
268
 
221
- ${ui_markdown2}`;
269
+ ${body_msg}`;
222
270
  return {
223
271
  content: [{ type: "text", text: llm_content2 }],
224
272
  structuredContent: {
225
- markdown: ui_markdown2,
273
+ markdown: body_msg,
226
274
  title: `Search: ${basename(file_path)}`,
227
275
  file_path: resolve(file_path)
228
276
  }
229
277
  };
230
278
  }
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).`);
279
+ const ui_parts = [];
280
+ if (filter_doc_page !== null) {
281
+ ui_parts.push(
282
+ `> **Search Results** \u2014 Found ${matches.length} match${matches.length !== 1 ? "es" : ""} for \`${search_query}\` on document page ${filter_doc_page} of ${total_doc_pages} in \`${basename(file_path)}\`.`
283
+ );
284
+ const otherPages = Array.from(pagesWithHits).filter((p) => p !== filter_doc_page).sort((a, b) => a - b);
285
+ if (otherPages.length > 0) {
286
+ ui_parts.push(
287
+ `> Additional matches exist on page${otherPages.length !== 1 ? "s" : ""} ${otherPages.join(", ")} \u2014 omit \`page\` or pass \`page='all'\` to see them.`
288
+ );
289
+ }
290
+ } else {
291
+ ui_parts.push(
292
+ `> **Search Results** \u2014 Found ${matches.length} match${matches.length !== 1 ? "es" : ""} for \`${search_query}\` in \`${basename(file_path)}\`.`
293
+ );
294
+ if (total_doc_pages > 1) {
295
+ const counts = /* @__PURE__ */ new Map();
296
+ for (const m of allMatches) {
297
+ const p = pageOfOffset(m.index);
298
+ counts.set(p, (counts.get(p) || 0) + 1);
299
+ }
300
+ const distribution = Array.from(counts.entries()).sort((a, b) => a[0] - b[0]).map(([p, n]) => `p${p}: ${n}`).join(", ");
301
+ ui_parts.push(
302
+ `> Distribution across ${total_doc_pages} document pages \u2014 ${distribution}. Pass \`page=N\` to filter to a specific document page.`
303
+ );
243
304
  }
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
305
  }
256
306
  const occurrences_map = {};
257
- for (const m of matches) {
307
+ for (const m of allMatches) {
258
308
  const matched_str = m[0];
259
309
  occurrences_map[matched_str] = (occurrences_map[matched_str] || 0) + 1;
260
310
  }
@@ -281,19 +331,12 @@ ${ui_markdown2}`;
281
331
  }
282
332
  return path.join(" > ");
283
333
  }
284
- let i = start_idx + 1;
285
- for (const m of page_matches) {
334
+ let i = 1;
335
+ for (const m of matches) {
286
336
  const matched_str = m[0];
287
337
  const m_start = m.index;
288
338
  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
- }
339
+ const p_num = pageOfOffset(m_start);
297
340
  const snippet_start = Math.max(0, m_start - 100);
298
341
  const snippet_end = Math.min(body.length, m_end + 100);
299
342
  const snippet = body.substring(snippet_start, m_start) + `**${matched_str}**` + body.substring(m_end, snippet_end);
@@ -306,7 +349,9 @@ ${ui_markdown2}`;
306
349
  }
307
350
  const count = occurrences_map[matched_str];
308
351
  ui_parts.push(snippet_lines);
309
- ui_parts.push(`*Occurrences:* This exact phrasing appears ${count} time${count !== 1 ? "s" : ""} in the document.`);
352
+ ui_parts.push(
353
+ `*Occurrences:* This exact phrasing appears ${count} time${count !== 1 ? "s" : ""} in the document.`
354
+ );
310
355
  i++;
311
356
  }
312
357
  const ui_markdown = ui_parts.join("\n\n");
@@ -1209,10 +1254,10 @@ function getAssetContent(folder, filename, fallbackMessage) {
1209
1254
  var READ_DOCX_COMMON_DESC = "Reads a DOCX file. Returns text with inline CriticMarkup for Tracked Changes and Comments: {++inserted++}, {--deleted--}, {==highlighted==}{>>comment<<}. Set clean_view=True for the finalized 'Accepted' text without markup.\n\n";
1210
1255
  var READ_DOCX_TAIL = "Modes:\n- 'full' (default): paginated body content. Use page=N to navigate.\n- 'outline': heading map only \u2014 start here for large docs to plan targeted reads. Defaults to L1-L2 headings; pass outline_max_level=3-6 to see deeper structure.\n- 'appendix': defined terms, anchors, and cross-reference targets. Consult before editing legal/technical docs to avoid breaking references.";
1211
1256
  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";
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.";
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.";
1214
- var gitSha = "8fc8371";
1215
- var packageVersion = "1.15.1";
1257
+ 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.\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.";
1258
+ 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.";
1259
+ var gitSha = "2179c76";
1260
+ var packageVersion = "1.16.0";
1216
1261
  var buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
1217
1262
  var args = process.argv.slice(2);
1218
1263
  var scopeIdx = args.indexOf("--scope");
@@ -1225,7 +1270,7 @@ var server = new McpServer({
1225
1270
  var originalRegisterTool = server.registerTool.bind(server);
1226
1271
  server.registerTool = (name, schema, handler) => {
1227
1272
  if (schema && typeof schema === "object") {
1228
- if (schema.description) {
1273
+ if (schema.description && !schema.description.includes(buildTag.trim())) {
1229
1274
  schema.description = schema.description.trim() + buildTag;
1230
1275
  }
1231
1276
  }
@@ -1315,11 +1360,17 @@ registerAppTool(
1315
1360
  mode: z.enum(["full", "outline", "appendix"]).default("full").describe(
1316
1361
  "'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms."
1317
1362
  ),
1318
- page: z.union([z.number(), z.string()]).default(1).describe("Page number (1-indexed) for mode='full', or 'all' to return unpaginated."),
1363
+ page: z.union([z.number(), z.string()]).optional().describe(
1364
+ "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)."
1365
+ ),
1319
1366
  outline_max_level: z.coerce.number().default(2).describe("For mode='outline' only: cap on heading depth."),
1320
1367
  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."),
1368
+ search_query: z.string().optional().describe(
1369
+ "The substring or regex pattern to search for. When provided, filters results to matching paragraphs."
1370
+ ),
1371
+ search_regex: z.boolean().default(false).describe(
1372
+ "Set to true to interpret search_query as a regular expression."
1373
+ ),
1323
1374
  search_case_sensitive: z.boolean().default(true).describe("Set to false to perform case-insensitive matching.")
1324
1375
  }),
1325
1376
  _meta: { ui: { resourceUri: MARKDOWN_UI_URI } }
@@ -1339,7 +1390,12 @@ registerAppTool(
1339
1390
  const buf = readFileBytesOrThrow(file_path);
1340
1391
  if (mode === "outline") {
1341
1392
  const doc = await DocumentObject2.load(buf);
1342
- const extract_res = _extractTextFromDoc(doc, clean_view, true, true);
1393
+ const extract_res = _extractTextFromDoc(
1394
+ doc,
1395
+ clean_view,
1396
+ true,
1397
+ true
1398
+ );
1343
1399
  const res2 = build_outline_response(
1344
1400
  doc,
1345
1401
  extract_res.text,
@@ -1352,14 +1408,22 @@ registerAppTool(
1352
1408
  }
1353
1409
  const text = await extractTextFromBuffer(buf, clean_view);
1354
1410
  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);
1411
+ const res2 = build_search_response(
1412
+ text,
1413
+ search_query,
1414
+ search_regex,
1415
+ search_case_sensitive,
1416
+ page,
1417
+ file_path
1418
+ );
1356
1419
  return res2;
1357
1420
  }
1421
+ const resolvedPage = page === void 0 || page === null ? 1 : typeof page === "number" ? page : parseInt(String(page), 10) || 1;
1358
1422
  if (mode === "appendix") {
1359
- const res2 = build_appendix_response(text, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
1423
+ const res2 = build_appendix_response(text, resolvedPage, file_path);
1360
1424
  return res2;
1361
1425
  }
1362
- const res = build_paginated_response(text, typeof page === "number" ? page : parseInt(page, 10) || 1, file_path);
1426
+ const res = build_paginated_response(text, resolvedPage, file_path);
1363
1427
  return res;
1364
1428
  } catch (e) {
1365
1429
  return {
@@ -1374,6 +1438,34 @@ registerAppTool(
1374
1438
  }
1375
1439
  }
1376
1440
  );
1441
+ var CHANGE_ITEM_SCHEMA = z.object({
1442
+ type: z.enum(["modify", "accept", "reject", "reply", "insert_row", "delete_row"]).describe(
1443
+ "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)."
1444
+ ),
1445
+ target_text: z.string().optional().describe(
1446
+ "modify / insert_row / delete_row: the existing text to locate (interpreted as a regex when regex=true)."
1447
+ ),
1448
+ new_text: z.string().optional().describe(
1449
+ "modify: replacement text. Supports Markdown (headings, **bold**, _italic_, '\\n\\n' paragraph splits); empty string deletes. Regex capture groups are available as $1, $2\u2026"
1450
+ ),
1451
+ target_id: z.string().optional().describe(
1452
+ "accept / reject / reply: the 'Chg:N' or 'Com:N' id taken from a fresh read_docx."
1453
+ ),
1454
+ text: z.string().optional().describe("reply: the reply body."),
1455
+ comment: z.string().optional().describe(
1456
+ "modify / accept / reject: attach a margin comment to the change (no manual CriticMarkup)."
1457
+ ),
1458
+ match_mode: z.enum(["strict", "first", "all"]).optional().describe(
1459
+ "modify only: 'strict' (default \u2014 target must match uniquely), 'first' (first occurrence), or 'all' (every occurrence)."
1460
+ ),
1461
+ regex: z.boolean().optional().describe(
1462
+ "modify only: treat target_text as a regular expression (default false)."
1463
+ ),
1464
+ position: z.enum(["above", "below"]).optional().describe(
1465
+ "insert_row: place the new row above or below the matched row."
1466
+ ),
1467
+ cells: z.array(z.string()).optional().describe("insert_row: the cell values for the new row, left to right.")
1468
+ }).passthrough();
1377
1469
  server.registerTool(
1378
1470
  "process_document_batch",
1379
1471
  {
@@ -1381,7 +1473,9 @@ server.registerTool(
1381
1473
  inputSchema: {
1382
1474
  original_docx_path: z.string().describe("Absolute path to the source file."),
1383
1475
  author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
1384
- changes: z.array(z.any()).describe("List of changes to apply. Each change must specify 'type'."),
1476
+ changes: z.array(z.union([z.string(), CHANGE_ITEM_SCHEMA])).describe(
1477
+ "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."
1478
+ ),
1385
1479
  output_path: z.string().optional().describe("Optional output path."),
1386
1480
  dry_run: z.boolean().optional().default(false).describe(
1387
1481
  "If True, simulates the changes and returns a detailed preview report without modifying any files."
@@ -1538,7 +1632,7 @@ server.registerTool(
1538
1632
  server.registerTool(
1539
1633
  "finalize_document",
1540
1634
  {
1541
- description: "Prepares a document for external distribution or e-signature.",
1635
+ 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.",
1542
1636
  inputSchema: {
1543
1637
  file_path: z.string().describe("Absolute path to the DOCX file."),
1544
1638
  output_path: z.string().optional().describe("Optional output path."),
@@ -1546,7 +1640,9 @@ server.registerTool(
1546
1640
  accept_all: z.boolean().optional().describe(
1547
1641
  "If true, auto-accepts all unresolved track changes before finalizing."
1548
1642
  ),
1549
- protection_mode: z.enum(["read_only", "encrypt"]).optional().describe("Native OOXML document locking."),
1643
+ protection_mode: z.enum(["read_only", "encrypt"]).optional().describe(
1644
+ "Native OOXML document locking. Note: 'encrypt' is unsupported in this zero-dependency build and falls back to 'read_only'."
1645
+ ),
1550
1646
  password: z.string().optional().describe("Ignored in this environment."),
1551
1647
  author: z.string().optional().describe("Replace all remaining markup authorship with this name."),
1552
1648
  export_pdf: z.boolean().optional().describe("Ignored in this environment.")
@@ -1707,7 +1803,10 @@ function formatBatchResult(stats, outPath, dry_run) {
1707
1803
  res = `Batch complete. Saved to: ${outPath}
1708
1804
  `;
1709
1805
  }
1710
- const total_occurrences = stats.edits ? stats.edits.reduce((acc, e) => acc + (e.status === "applied" ? e.occurrences_modified || 1 : 0), 0) : 0;
1806
+ const total_occurrences = stats.edits ? stats.edits.reduce(
1807
+ (acc, e) => acc + (e.status === "applied" ? e.occurrences_modified || 1 : 0),
1808
+ 0
1809
+ ) : 0;
1711
1810
  const occ_text = total_occurrences > stats.edits_applied ? ` (${total_occurrences} occurrences)` : "";
1712
1811
  res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.
1713
1812
  `;
@@ -1760,8 +1859,8 @@ ${stats.skipped_details.join("\n")}`;
1760
1859
  async function main() {
1761
1860
  const transport = new StdioServerTransport();
1762
1861
  await server.connect(transport);
1763
- const gitSha2 = "8fc8371";
1764
- const buildTs = "2026-06-25T11:57:32.023Z";
1862
+ const gitSha2 = "2179c76";
1863
+ const buildTs = "2026-06-26T07:48:21.832Z";
1765
1864
  console.error(
1766
1865
  `Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha2}@${buildTs}`
1767
1866
  );