@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 +179 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/formatter.qa.test.ts +71 -0
- package/src/formatter.test.ts +12 -6
- package/src/index.ts +76 -16
- package/src/mcp.bugs.test.ts +28 -0
- package/src/response-builders.ts +151 -0
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 = "
|
|
1093
|
-
var packageVersion = "1.
|
|
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'
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1724
|
+
if (report.heading_path) {
|
|
1725
|
+
res += `**Path:** \`${report.heading_path}\`
|
|
1578
1726
|
`;
|
|
1579
|
-
|
|
1580
|
-
|
|
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 +=
|
|
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 +=
|
|
1742
|
+
res += `*Preview (CriticMarkup):*
|
|
1743
|
+
> ${report.critic_markup.split("\\n").join("\\n> ")}
|
|
1589
1744
|
`;
|
|
1590
1745
|
}
|
|
1591
1746
|
if (report.clean_text) {
|
|
1592
|
-
res +=
|
|
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 = "
|
|
1609
|
-
const buildTs = "2026-06-
|
|
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
|
);
|