@adeu/mcp-server 1.27.0 → 1.29.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 +48 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +33 -11
- package/src/repro_agy_bug.test.ts +117 -0
- package/src/repro_agy_changes_json.test.ts +138 -0
- package/src/repro_agy_search_query_filter.test.ts +129 -0
- package/src/repro_mcp_qa_issues_2026_07_20.test.ts +29 -0
- package/src/response-builders.ts +21 -4
- package/tests/fixtures/generate_fixtures.py +32 -26
package/dist/index.js
CHANGED
|
@@ -64,7 +64,7 @@ function _build_appendix_pointer(has_appendix) {
|
|
|
64
64
|
}
|
|
65
65
|
function _build_page_banner(page, total) {
|
|
66
66
|
if (total <= 1) return "";
|
|
67
|
-
return `> **Page ${page} of ${total}** \u2014 call \`read_docx\` with \`mode='outline'\` for a heading map of the full document.
|
|
67
|
+
return `> **Page ${page} of ${total}** (synthetic page \u2014 a length-based chunk, not a printed Word page) \u2014 call \`read_docx\` with \`mode='outline'\` for a heading map of the full document.
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
@@ -372,6 +372,14 @@ ${body_msg}`;
|
|
|
372
372
|
const matched_str = m[0];
|
|
373
373
|
occurrences_map[matched_str] = (occurrences_map[matched_str] || 0) + 1;
|
|
374
374
|
}
|
|
375
|
+
const max_matches = 20;
|
|
376
|
+
const is_truncated = matches.length > max_matches;
|
|
377
|
+
const items_to_render = matches.slice(0, max_matches);
|
|
378
|
+
if (is_truncated) {
|
|
379
|
+
ui_parts.push(
|
|
380
|
+
`> **Note:** Only the first ${max_matches} matches are shown here to prevent LLM context overflow. Narrow your search query or specify a \`page\` filter to see other matches.`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
375
383
|
function get_heading(idx, txt) {
|
|
376
384
|
const txtBefore = txt.substring(0, idx);
|
|
377
385
|
const lines = txtBefore.split("\n");
|
|
@@ -396,13 +404,15 @@ ${body_msg}`;
|
|
|
396
404
|
return path.join(" > ");
|
|
397
405
|
}
|
|
398
406
|
let i = 1;
|
|
399
|
-
for (const m of
|
|
407
|
+
for (const m of items_to_render) {
|
|
400
408
|
const matched_str = m[0];
|
|
401
409
|
const m_start = m.index;
|
|
402
410
|
const m_end = m_start + matched_str.length;
|
|
403
411
|
const p_num = pageOfOffset(m_start);
|
|
404
|
-
const
|
|
405
|
-
const
|
|
412
|
+
const lastNL = m_start <= 0 ? -1 : body.lastIndexOf("\n", m_start - 1);
|
|
413
|
+
const snippet_start = lastNL === -1 ? 0 : lastNL + 1;
|
|
414
|
+
const nextNL = body.indexOf("\n", m_end);
|
|
415
|
+
const snippet_end = nextNL === -1 ? body.length : nextNL;
|
|
406
416
|
const snippet = emphasizedSnippet(
|
|
407
417
|
body.substring(snippet_start, m_start),
|
|
408
418
|
matched_str,
|
|
@@ -521,8 +531,8 @@ var READ_DOCX_TAIL = "Modes:\n- 'full' (default): paginated body content. Use pa
|
|
|
521
531
|
var PROCESS_BATCH_COMMON_DESC = "Applies a batch of edits and review actions to a DOCX.\n\nBatches apply SEQUENTIALLY: each change is validated and applied against the document state produced by the changes before it, so you may chain dependent edits within one batch (e.g. rename X to Y, then modify Y \u2014 the second edit must target Y, the text as it reads after the rename). Validation failures reject the whole batch transactionally: nothing is applied until every change resolves.\n\n";
|
|
522
532
|
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.";
|
|
523
533
|
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.";
|
|
524
|
-
var gitSha = "
|
|
525
|
-
var packageVersion = "1.
|
|
534
|
+
var gitSha = "6fc8525";
|
|
535
|
+
var packageVersion = "1.29.0";
|
|
526
536
|
var buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
|
|
527
537
|
var server = new McpServer({
|
|
528
538
|
name: "adeu-redlining-service",
|
|
@@ -736,6 +746,13 @@ server.registerTool(
|
|
|
736
746
|
),
|
|
737
747
|
original_docx_path: z.string().describe("Absolute path to the source file."),
|
|
738
748
|
author_name: z.string().describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
|
|
749
|
+
// Deliberately a plain REQUIRED array of typed items. Wrapping this in
|
|
750
|
+
// z.preprocess (to also accept the whole array as one JSON string) drops
|
|
751
|
+
// it out of the schema's `required` list, and a z.union publishes an
|
|
752
|
+
// anyOf that hides the item schema — both cost more, on every call, than
|
|
753
|
+
// they buy for the rare client that stringifies its payload. That client
|
|
754
|
+
// gets a clear "expected array, received string" it can retry from.
|
|
755
|
+
// Per-item stringification is still tolerated, below and in the engine.
|
|
739
756
|
changes: z.array(z.union([z.string(), CHANGE_ITEM_SCHEMA])).describe(
|
|
740
757
|
"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). Items apply SEQUENTIALLY: each one evaluates against the document state produced by the items before it, so later items may target text an earlier item introduced."
|
|
741
758
|
),
|
|
@@ -1024,17 +1041,28 @@ server.registerTool(
|
|
|
1024
1041
|
author,
|
|
1025
1042
|
export_pdf
|
|
1026
1043
|
});
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1044
|
+
if (result.outBuffer) {
|
|
1045
|
+
fs.writeFileSync(outPath, result.outBuffer);
|
|
1046
|
+
return {
|
|
1047
|
+
content: [
|
|
1048
|
+
{
|
|
1049
|
+
type: "text",
|
|
1050
|
+
text: `Saved to: ${outPath}
|
|
1033
1051
|
|
|
1034
1052
|
${result.reportText}`
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1053
|
+
}
|
|
1054
|
+
]
|
|
1055
|
+
};
|
|
1056
|
+
} else {
|
|
1057
|
+
return {
|
|
1058
|
+
content: [
|
|
1059
|
+
{
|
|
1060
|
+
type: "text",
|
|
1061
|
+
text: result.reportText
|
|
1062
|
+
}
|
|
1063
|
+
]
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1038
1066
|
} catch (e) {
|
|
1039
1067
|
return {
|
|
1040
1068
|
isError: true,
|
|
@@ -1057,7 +1085,9 @@ function formatBatchResult(stats, outPath, dry_run) {
|
|
|
1057
1085
|
0
|
|
1058
1086
|
) : 0;
|
|
1059
1087
|
const occ_text = total_occurrences > stats.edits_applied ? ` (${total_occurrences} occurrences)` : "";
|
|
1060
|
-
|
|
1088
|
+
const already = stats.actions_already_resolved || 0;
|
|
1089
|
+
const already_text = already ? `, ${already} already resolved (no effect)` : "";
|
|
1090
|
+
res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped${already_text}.
|
|
1061
1091
|
`;
|
|
1062
1092
|
res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.
|
|
1063
1093
|
`;
|
|
@@ -1113,8 +1143,8 @@ async function main() {
|
|
|
1113
1143
|
}
|
|
1114
1144
|
const transport = new StdioServerTransport();
|
|
1115
1145
|
await server.connect(transport);
|
|
1116
|
-
const gitSha2 = "
|
|
1117
|
-
const buildTs = "2026-07-
|
|
1146
|
+
const gitSha2 = "6fc8525";
|
|
1147
|
+
const buildTs = "2026-07-22T16:22:20.983Z";
|
|
1118
1148
|
console.error(
|
|
1119
1149
|
`Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha2}@${buildTs}`
|
|
1120
1150
|
);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/response-builders.ts","../src/shared.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { basename, resolve, extname, dirname, join } from \"node:path\";\nimport { z } from \"zod\";\nimport {\n registerAppTool as origRegisterAppTool,\n registerAppResource,\n RESOURCE_MIME_TYPE,\n} from \"@modelcontextprotocol/ext-apps/server\";\nimport fs from \"node:fs\";\nimport {\n identifyEngine,\n extractTextFromBuffer,\n _extractTextFromDoc,\n DocumentObject,\n RedlineEngine,\n BatchValidationError,\n create_word_patch_diff,\n collect_media_difference_warnings,\n finalize_document,\n} from \"@adeu/core\";\nimport { describe_illegal_control_chars } from \"@adeu/core\";\n\nimport {\n build_paginated_response,\n build_full_document_response,\n build_outline_response,\n build_appendix_response,\n build_search_response,\n} from \"./response-builders.js\";\n\nimport { MARKDOWN_UI_URI, handleServerCliArgs } from \"./shared.js\";\n// Parity with Python models.py `_infer_type_in_place` + `_coerce_match_mode_in_place`.\n// The MCP boundary schema is permissive; these repairs let recoverable payloads\n// (a missing `type` that's unambiguous from the key signature, or a non-canonical\n// `match_mode`) succeed instead of failing the whole-array Zod parse with an\n// opaque -32602. Anything still un-inferrable is caught by the handler guard\n// below and reported per-index; anything that doesn't apply to the document is\n// caught by the engine's validate_edits. Mirrors how Python repairs in a\n// BeforeValidator ahead of its (strict) discriminated union.\nconst MATCH_MODE_SYNONYMS: Record<string, \"strict\" | \"first\" | \"all\"> = {\n strict: \"strict\",\n first: \"first\",\n all: \"all\",\n first_only: \"first\",\n firstonly: \"first\",\n \"first-only\": \"first\",\n all_occurrences: \"all\",\n alloccurrences: \"all\",\n \"all-occurrences\": \"all\",\n every: \"all\",\n};\n\nfunction coerceChangeItemInPlace(item: any): void {\n if (item === null || typeof item !== \"object\" || Array.isArray(item)) return;\n\n // Infer a missing `type` ONLY when exactly one variant fits unambiguously.\n // Deliberately do NOT infer from `target_id` alone (accept vs reject is a\n // semantic choice) or `target_text` alone (delete_row vs empty-new_text\n // modify). Those stay absent and are rejected with a clear message.\n if (!(\"type\" in item) || item.type === undefined || item.type === null) {\n if (\"cells\" in item) item.type = \"insert_row\";\n else if (\"text\" in item && \"target_id\" in item) item.type = \"reply\";\n else if (\"target_text\" in item && \"new_text\" in item) item.type = \"modify\";\n }\n\n // Normalize match_mode: canonical passes through, synonyms map, anything else\n // (help-string echo \"strict, first, or all\", empty, non-string) is dropped so\n // the engine's \"strict\" default applies. Never coerce junk to \"all\" — that\n // would silently mass-edit; defaulting to strict fails safe with an\n // ambiguity error instead.\n if (\"match_mode\" in item) {\n const raw = item.match_mode;\n if (typeof raw !== \"string\") {\n delete item.match_mode;\n } else {\n const mapped = MATCH_MODE_SYNONYMS[raw.trim().toLowerCase()];\n if (mapped === undefined) delete item.match_mode;\n else item.match_mode = mapped;\n }\n }\n}\nfunction readFileBytesOrThrow(filePath: string): Buffer {\n try {\n return readFileSync(filePath);\n } catch (err: any) {\n if (err.code === \"ENOENT\") {\n // Lean, agent-appropriate error: list sibling .docx files so the model\n // can self-correct a wrong filename (e.g. a guessed `-processed` suffix)\n // in one turn, instead of being handed CLI install instructions that are\n // irrelevant inside an agent loop and pure token waste.\n let available = \"\";\n try {\n const dir = dirname(filePath);\n const docs = fs\n .readdirSync(dir)\n .filter((f) => f.toLowerCase().endsWith(\".docx\"));\n available = docs.length\n ? ` available files: [${docs.join(\", \")}]`\n : ` (no .docx files found in ${dir})`;\n } catch {\n // Directory unreadable — omit the listing rather than fail.\n }\n throw new Error(`file not found: ${basename(filePath)};${available}`);\n }\n throw err;\n }\n}\n\n// --- Asset Loaders for UI ---\nconst DIST_DIR = import.meta.dirname;\n\nfunction getAssetContent(\n folder: \"templates\" | \"assets\",\n filename: string,\n fallbackMessage: string,\n): string {\n const filePath = join(DIST_DIR, folder, filename);\n if (existsSync(filePath)) {\n return readFileSync(filePath, \"utf-8\");\n }\n return fallbackMessage;\n}\n\n// --- Tool Description Constants ---\nconst READ_DOCX_COMMON_DESC =\n \"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\";\nconst READ_DOCX_TAIL =\n \"Modes:\\n- 'full' (default): paginated body content. Use page=N to navigate.\\n- 'outline': heading map only — 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.\";\n\nconst PROCESS_BATCH_COMMON_DESC =\n \"Applies a batch of edits and review actions to a DOCX.\\n\\nBatches apply SEQUENTIALLY: each change is validated and applied against the document state produced by the changes before it, so you may chain dependent edits within one batch (e.g. rename X to Y, then modify Y — the second edit must target Y, the text as it reads after the rename). Validation failures reject the whole batch transactionally: nothing is applied until every change resolves.\\n\\n\";\nconst PROCESS_BATCH_OPERATIONS_DESC =\n \"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.\\n • EMPTY/FORM TABLE CELLS: a blank cell has no text to match. `read_docx` renders each cell with a trailing `{#cell:<id>}` anchor — 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 — 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. 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.\";\n\nconst DIFF_DOCX_DESC =\n \"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.\";\n\nconst gitSha = process.env.GIT_SHA || \"unknown\";\nconst packageVersion = process.env.PACKAGE_VERSION || \"unknown\";\nconst buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;\n\n// --- Server Setup ---\nconst server = new McpServer({\n name: \"adeu-redlining-service\",\n version: packageVersion,\n});\n\n// Wrap server.registerTool to inject buildTag into descriptions\nconst originalRegisterTool = server.registerTool.bind(server);\nserver.registerTool = (name: string, schema: any, handler?: any) => {\n if (schema && typeof schema === \"object\") {\n // Idempotent: UI tools route through BOTH this wrapper and the\n // registerAppTool wrapper, so guard against stamping the tag twice.\n if (schema.description && !schema.description.includes(buildTag.trim())) {\n schema.description = schema.description.trim() + buildTag;\n }\n }\n return originalRegisterTool(name, schema, handler);\n};\n\n// Wrap registerAppTool to inject buildTag into descriptions\nconst registerAppTool: typeof origRegisterAppTool = (\n mcpServer,\n name,\n schema,\n handler,\n) => {\n if (schema && typeof schema === \"object\") {\n if (schema.description) {\n schema.description = schema.description.trim() + buildTag;\n }\n }\n return origRegisterAppTool(mcpServer, name, schema, handler);\n};\n\n// Common CSP allowing Google Fonts used by Adeu UI templates\nconst UI_CSP = {\n connectDomains: [\"https://fonts.googleapis.com\", \"https://fonts.gstatic.com\"],\n resourceDomains: [\n \"https://fonts.googleapis.com\",\n \"https://fonts.gstatic.com\",\n ],\n};\n\n// ==========================================\n// 1. UI RESOURCES\n// ==========================================\n\nregisterAppResource(\n server,\n MARKDOWN_UI_URI,\n MARKDOWN_UI_URI,\n { mimeType: RESOURCE_MIME_TYPE, description: \"Adeu Markdown Viewer UI\" },\n async () => {\n let html = getAssetContent(\n \"templates\",\n \"markdown_ui.html\",\n \"<html><body>UI Template Not Found</body></html>\",\n );\n const markedJs = getAssetContent(\n \"assets\",\n \"marked.min.js\",\n \"window.__MARKED_ERROR = 'marked.min.js not found';\",\n );\n const svg = getAssetContent(\"assets\", \"adeu.svg\", \"\");\n\n html = html\n .replace(\"[[marked_js_code | safe]]\", markedJs)\n .replace(\"[[ adeu_svg_code ]]\", svg);\n\n return {\n contents: [\n {\n uri: MARKDOWN_UI_URI,\n mimeType: RESOURCE_MIME_TYPE,\n text: html,\n _meta: { ui: { csp: UI_CSP } },\n },\n ],\n };\n },\n);\n\n// ==========================================\n// 2. UI-ENABLED TOOLS\n// ==========================================\nregisterAppTool(\n server,\n \"read_docx\",\n {\n title: \"Read DOCX\",\n description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,\n inputSchema: z.object({\n reasoning: z\n .string()\n .describe(\n \"Why do I need to read this docx document? State this reason before any other parameter.\",\n ),\n file_path: z.string().describe(\"Absolute path to the DOCX file.\"),\n clean_view: z\n .boolean()\n .default(false)\n .describe(\n \"If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text.\",\n ),\n mode: z\n .enum([\"full\", \"outline\", \"appendix\"])\n .default(\"full\")\n .describe(\n \"'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms.\",\n ),\n page: z\n .union([z.number(), z.string()])\n .optional()\n .describe(\n \"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). Note: pages are synthetic, length-based content chunks sized for LLM consumption — they do NOT correspond to printed Word pages or explicit page breaks.\",\n ),\n outline_max_level: z.coerce\n .number()\n .default(2)\n .describe(\"For mode='outline' only: cap on heading depth.\"),\n outline_verbose: z\n .boolean()\n .default(false)\n .describe(\"For mode='outline' only: includes metadata.\"),\n search_query: z\n .string()\n .optional()\n .describe(\n \"The substring or regex pattern to search for. When provided, filters results to matching paragraphs.\",\n ),\n search_regex: z\n .boolean()\n .default(false)\n .describe(\n \"Set to true to interpret search_query as a regular expression.\",\n ),\n search_case_sensitive: z\n .boolean()\n .default(true)\n .describe(\"Set to false to perform case-insensitive matching.\"),\n }),\n _meta: { ui: { resourceUri: MARKDOWN_UI_URI } },\n },\n async ({\n reasoning,\n file_path,\n clean_view,\n mode,\n page,\n outline_max_level,\n outline_verbose,\n search_query,\n search_regex,\n search_case_sensitive,\n }) => {\n try {\n void reasoning;\n const buf = readFileBytesOrThrow(file_path);\n\n if (mode === \"outline\") {\n const doc = await DocumentObject.load(buf);\n const extract_res = _extractTextFromDoc(\n doc,\n clean_view,\n true,\n true,\n ) as {\n text: string;\n paragraph_offsets: Map<any, [number, number]>;\n };\n const res = build_outline_response(\n doc,\n extract_res.text,\n file_path,\n outline_max_level,\n outline_verbose,\n extract_res.paragraph_offsets,\n );\n return res as any;\n }\n\n const text = await extractTextFromBuffer(buf, clean_view);\n if (search_query !== undefined && search_query !== null) {\n // In search mode, undefined `page` means \"search all document pages\".\n const res = build_search_response(\n text,\n search_query,\n search_regex,\n search_case_sensitive,\n page,\n file_path,\n );\n return res as any;\n }\n // In full mode, page='all' returns the entire document without page\n // chrome — the round-trip artifact for text-based apply/diff\n // (QA 2026-07-17 F1 parity with the Python CLI's --page all).\n if (\n mode === \"full\" &&\n page !== undefined &&\n page !== null &&\n String(page).trim().toLowerCase() === \"all\"\n ) {\n const res = build_full_document_response(text, file_path);\n return res as any;\n }\n // In non-search mode, `page` defaults to 1 (show document page 1).\n // Non-numeric values must error, not silently fall back to page 1\n // (QA L1 parity with the Python CLI).\n let resolvedPage = 1;\n if (page !== undefined && page !== null) {\n const parsed =\n typeof page === \"number\" ? page : parseInt(String(page).trim(), 10);\n if (!Number.isFinite(parsed)) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text:\n `Invalid page value: '${page}'. Provide a positive integer ` +\n `(pages are 1-indexed; 'all' is valid for mode='full' and together with search_query).`,\n },\n ],\n };\n }\n resolvedPage = parsed;\n }\n if (mode === \"appendix\") {\n const res = build_appendix_response(text, resolvedPage, file_path);\n return res as any;\n }\n const res = build_paginated_response(text, resolvedPage, file_path);\n return res as any;\n } catch (e: any) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Error executing tool read_docx: ${e.message}`,\n },\n ],\n };\n }\n },\n);\n\n// ==========================================\n// 3. HEADLESS TOOLS (No UI)\n// ==========================================\n\n// Typed shape for a single `process_document_batch` change. This makes the six\n// DocumentChange variants — and the modify-only `match_mode`/`regex` options —\n// discoverable from the tool schema itself, instead of prose alone. A bare\n// string is still accepted (and normalized in-handler) so double-serialized\n// payloads from some LLM clients keep working; only `type` is required, all\n// other fields are optional, and unknown keys pass through untouched.\nconst CHANGE_ITEM_SCHEMA = z\n .object({\n type: z\n .enum([\"modify\", \"accept\", \"reject\", \"reply\", \"insert_row\", \"delete_row\"])\n .optional()\n .describe(\n \"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.\",\n ),\n target_text: z\n .string()\n .optional()\n .describe(\n \"modify / insert_row / delete_row: the existing text to locate (interpreted as a regex when regex=true).\",\n ),\n new_text: z\n .string()\n .optional()\n .describe(\n \"modify: replacement text. Supports Markdown (headings, **bold**, _italic_, '\\\\n\\\\n' paragraph splits); empty string deletes. Regex capture groups are available as $1, $2…\",\n ),\n target_id: z\n .string()\n .optional()\n .describe(\n \"accept / reject / reply: the 'Chg:N' or 'Com:N' id taken from a fresh read_docx.\",\n ),\n text: z.string().optional().describe(\"reply: the reply body.\"),\n comment: z\n .string()\n .optional()\n .describe(\n \"modify / accept / reject: attach a margin comment to the change (no manual CriticMarkup).\",\n ),\n match_mode: z\n .enum([\"strict\", \"first\", \"all\"])\n .optional()\n .describe(\n \"modify only: 'strict' (default — target must match uniquely), 'first' (first occurrence), or 'all' (every occurrence).\",\n ),\n regex: z\n .boolean()\n .optional()\n .describe(\n \"modify only: treat target_text as a regular expression (default false).\",\n ),\n position: z\n .enum([\"above\", \"below\"])\n .optional()\n .describe(\n \"insert_row: place the new row above or below the matched row.\",\n ),\n cells: z\n .array(z.string())\n .optional()\n .describe(\"insert_row: the cell values for the new row, left to right.\"),\n })\n .passthrough();\n\nserver.registerTool(\n \"process_document_batch\",\n {\n description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to apply these changes to the document? State this reason before any other parameter.\",\n ),\n original_docx_path: z\n .string()\n .describe(\"Absolute path to the source file.\"),\n author_name: z\n .string()\n .describe(\"Name to appear in Track Changes (e.g., 'Reviewer AI').\"),\n changes: z\n .array(z.union([z.string(), CHANGE_ITEM_SCHEMA]))\n .describe(\n \"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). Items apply SEQUENTIALLY: each one evaluates against the document state produced by the items before it, so later items may target text an earlier item introduced.\",\n ),\n output_path: z.string().optional().describe(\"Optional output path.\"),\n dry_run: z\n .boolean()\n .optional()\n .default(false)\n .describe(\n \"If True, simulates the changes and returns a detailed preview report without modifying any files.\",\n ),\n },\n },\n async ({\n reasoning,\n original_docx_path,\n author_name,\n changes,\n output_path,\n dry_run,\n }) => {\n try {\n void reasoning;\n if (!author_name || !author_name.trim())\n return {\n content: [\n { type: \"text\", text: \"Error: author_name cannot be empty.\" },\n ],\n };\n const author_ctrl = describe_illegal_control_chars(author_name);\n if (author_ctrl)\n return {\n content: [\n {\n type: \"text\",\n text:\n `Error: author_name contains control character(s) (${author_ctrl}) ` +\n `that cannot be stored in a DOCX. Remove them and retry.`,\n },\n ],\n };\n if (!changes || changes.length === 0)\n return {\n content: [{ type: \"text\", text: \"Error: No changes provided.\" }],\n };\n\n // Defensive sanitization at the MCP boundary: some LLM clients\n // \"double-serialize\" nested arrays, delivering each element of `changes`\n // as a JSON string instead of an object. The core engine also guards\n // against this, but we normalize here too so the tool layer never hands\n // raw string primitives downstream regardless of the engine version\n // bundled. Genuine objects and unparseable strings pass through\n // untouched so validation surfaces a clear error rather than crashing.\n const sanitizedChanges = changes.map((item: any) => {\n let obj: any = item;\n if (typeof item === \"string\") {\n try {\n const parsed = JSON.parse(item);\n obj = parsed !== null && typeof parsed === \"object\" ? parsed : item;\n } catch {\n obj = item;\n }\n }\n // Repair recoverable payloads (infer type, normalize match_mode) the\n // same way Python does before its union validation.\n if (obj !== null && typeof obj === \"object\" && !Array.isArray(obj)) {\n coerceChangeItemInPlace(obj);\n }\n return obj;\n });\n\n // Boundary guard, scoped narrowly: after inference, reject only an OBJECT\n // that still carries no resolvable `type`. Strings, nulls, and non-objects\n // are intentionally left for the engine's validate_edits to report\n // (\"Invalid change format… received a primitive\"), keeping the engine the\n // single authority for those and avoiding a competing error surface.\n // A typeless object is the one case the engine can't cleanly reject (with\n // `type` now optional it would fall into the edits bucket as a no-op), so\n // it is caught here with an actionable, per-index message.\n const VALID_TYPES = new Set([\n \"modify\",\n \"accept\",\n \"reject\",\n \"reply\",\n \"insert_row\",\n \"delete_row\",\n ]);\n const typeErrors: string[] = [];\n sanitizedChanges.forEach((c: any, i: number) => {\n if (\n c !== null &&\n typeof c === \"object\" &&\n !Array.isArray(c) &&\n (!c.type || !VALID_TYPES.has(c.type))\n ) {\n typeErrors.push(\n `- 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(\", \")}].`,\n );\n }\n });\n if (typeErrors.length > 0) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Batch rejected. Some changes are malformed:\\n\\n${typeErrors.join(\"\\n\")}`,\n },\n ],\n };\n }\n\n let outPath = output_path;\n if (!outPath) {\n const ext = extname(original_docx_path);\n const base = basename(original_docx_path, ext);\n const dir = dirname(original_docx_path);\n // Idempotency guard (parity with Python document.py): if the input is\n // already a processed artifact, write back to it instead of compounding\n // the suffix into contract_processed_processed.docx, which fragments the\n // agent's document state across files.\n if (base.endsWith(\"_processed\") || base.endsWith(\"_redlined\")) {\n outPath = resolve(dir, `${base}${ext}`);\n } else {\n outPath = resolve(dir, `${base}_processed${ext}`);\n }\n }\n\n const buf = readFileBytesOrThrow(original_docx_path);\n const doc = await DocumentObject.load(buf);\n const engine = new RedlineEngine(doc, author_name);\n\n let stats;\n try {\n stats = engine.process_batch(sanitizedChanges, dry_run);\n } catch (e: any) {\n if (e instanceof BatchValidationError) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Batch rejected. Some edits failed validation:\\n\\n${e.errors.join(\"\\n\\n\")}`,\n },\n ],\n };\n }\n throw e;\n }\n\n if (!dry_run) {\n const outBuf = await doc.save();\n try {\n fs.writeFileSync(outPath, outBuf);\n } catch (e: any) {\n // Filesystem failures (name too long, missing directory, perms)\n // must surface as a clear, actionable error (QA H3 parity).\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Could not write output file '${outPath}': ${e.message}`,\n },\n ],\n };\n }\n }\n\n let res = formatBatchResult(stats, outPath, !!dry_run);\n if (sanitizedChanges.length === 0) {\n res =\n `⚠️ 0 changes provided — nothing to do. The output is an unmodified copy of the original.\\n\\n` +\n res;\n }\n return { content: [{ type: \"text\", text: res }] };\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\nserver.registerTool(\n \"accept_all_changes\",\n {\n description:\n \"Accepts all tracked changes and removes all comments in a single operation.\",\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to accept all changes in this document? State this reason before any other parameter.\",\n ),\n docx_path: z.string().describe(\"Absolute path to the DOCX file.\"),\n output_path: z.string().optional().describe(\"Optional output path.\"),\n },\n },\n async ({ reasoning, docx_path, output_path }) => {\n try {\n void reasoning;\n let outPath = output_path;\n if (!outPath) {\n const ext = extname(docx_path);\n const base = basename(docx_path, ext);\n const dir = dirname(docx_path);\n outPath = resolve(dir, `${base}_clean${ext}`);\n }\n\n const buf = readFileBytesOrThrow(docx_path);\n const doc = await DocumentObject.load(buf);\n const engine = new RedlineEngine(doc);\n\n engine.accept_all_revisions();\n\n const outBuf = await doc.save();\n\n fs.writeFileSync(outPath, outBuf);\n\n return {\n content: [\n { type: \"text\", text: `Accepted all changes. Saved to: ${outPath}` },\n ],\n };\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\nserver.registerTool(\n \"diff_docx_files\",\n {\n description: DIFF_DOCX_DESC,\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to diff these two documents? State this reason before any other parameter.\",\n ),\n original_path: z\n .string()\n .describe(\"Absolute path to the baseline DOCX file.\"),\n modified_path: z\n .string()\n .describe(\"Absolute path to the modified DOCX file.\"),\n compare_clean: z\n .boolean()\n .default(true)\n .describe(\n \"If True, compares 'Accepted' state. If False, compares raw text.\",\n ),\n },\n },\n async ({ reasoning, original_path, modified_path, compare_clean }) => {\n try {\n void reasoning;\n const origBuf = readFileBytesOrThrow(original_path);\n const modBuf = readFileBytesOrThrow(modified_path);\n\n // includeAppendix=false: the generated appendix (\"used N times\",\n // diagnostics) is not document content — diffing it produces phantom\n // changes no apply can consume (QA 2026-07-18 H1).\n const origText = await extractTextFromBuffer(origBuf, compare_clean, false);\n const modText = await extractTextFromBuffer(modBuf, compare_clean, false);\n\n const diff = create_word_patch_diff(\n origText,\n modText,\n basename(original_path),\n basename(modified_path),\n );\n\n // A text diff cannot see image bytes: when embedded media differ, an\n // empty diff must never read as \"the documents are identical\"\n // (QA 2026-07-19 F-04).\n const media_warnings = collect_media_difference_warnings(\n new Uint8Array(origBuf),\n new Uint8Array(modBuf),\n );\n const warning_text = media_warnings.length\n ? media_warnings.map((w) => `⚠️ ${w}`).join(\"\\n\") + \"\\n\\n\"\n : \"\";\n\n return {\n content: [\n {\n type: \"text\",\n text: warning_text + (diff || \"No differences found.\"),\n },\n ],\n };\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\nserver.registerTool(\n \"finalize_document\",\n {\n description:\n \"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.\",\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to finalize this document? State this reason before any other parameter.\",\n ),\n file_path: z.string().describe(\"Absolute path to the DOCX file.\"),\n output_path: z.string().optional().describe(\"Optional output path.\"),\n sanitize_mode: z\n .enum([\"full\", \"keep-markup\"])\n .optional()\n .describe(\"full removes all markup, keep-markup redacts metadata.\"),\n accept_all: z\n .boolean()\n .optional()\n .describe(\n \"If true, auto-accepts all unresolved track changes before finalizing.\",\n ),\n protection_mode: z\n .enum([\"read_only\", \"encrypt\"])\n .optional()\n .describe(\n \"Native OOXML document locking. Note: 'encrypt' is unsupported in this zero-dependency build and falls back to 'read_only'.\",\n ),\n password: z.string().optional().describe(\"Ignored in this environment.\"),\n author: z\n .string()\n .optional()\n .describe(\"Replace all remaining markup authorship with this name.\"),\n export_pdf: z\n .boolean()\n .optional()\n .describe(\"Ignored in this environment.\"),\n },\n },\n async ({\n reasoning,\n file_path,\n output_path,\n sanitize_mode,\n accept_all,\n protection_mode,\n author,\n export_pdf,\n }) => {\n try {\n void reasoning;\n let outPath = output_path;\n if (!outPath) {\n const ext = extname(file_path);\n const base = basename(file_path, ext);\n const dir = dirname(file_path);\n outPath = resolve(dir, `${base}_final${ext}`);\n }\n\n const buf = readFileBytesOrThrow(file_path);\n const doc = await DocumentObject.load(buf);\n\n const result = await finalize_document(doc, {\n filename: basename(file_path),\n sanitize_mode: (sanitize_mode as any) || \"full\",\n accept_all: accept_all as boolean,\n protection_mode: protection_mode as any,\n author: author as string,\n export_pdf: export_pdf as boolean,\n });\n\n fs.writeFileSync(outPath, result.outBuffer!);\n\n return {\n content: [\n {\n type: \"text\",\n text: `Saved to: ${outPath}\\n\\n${result.reportText}`,\n },\n ],\n };\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\n// --- Formatter for process_document_batch ---\nexport function formatBatchResult(\n stats: any,\n outPath: string,\n dry_run: boolean,\n): string {\n let res = \"\";\n if (dry_run) {\n res = `Dry-run simulation complete.\\n`;\n } else {\n res = `Batch complete. Saved to: ${outPath}\\n`;\n }\n const total_occurrences = stats.edits\n ? stats.edits.reduce(\n (acc: number, e: any) =>\n acc + (e.status === \"applied\" ? e.occurrences_modified || 1 : 0),\n 0,\n )\n : 0;\n const occ_text =\n total_occurrences > stats.edits_applied\n ? ` (${total_occurrences} occurrences)`\n : \"\";\n\n res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped.\\n`;\n res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.\\n`;\n\n if (stats.edits && stats.edits.length > 0) {\n res += \"\\nDetailed Edit Reports:\\n\";\n for (let i = 0; i < stats.edits.length; i++) {\n const report = stats.edits[i];\n const status_indicator =\n report.status === \"applied\" ? \"✅ [applied]\" : \"❌ [failed]\";\n\n const pagesStr =\n report.pages && report.pages.length > 0\n ? ` (p${report.pages.join(\", p\")})`\n : \"\";\n\n res += `### Edit ${i + 1} ${status_indicator}${pagesStr}\\n`;\n\n if (report.heading_path) {\n res += `**Path:** \\`${report.heading_path}\\`\\n`;\n }\n\n if (report.match_mode) {\n const occ =\n report.occurrences_modified || (report.status === \"applied\" ? 1 : 0);\n res += `**Mode:** \\`${report.match_mode}\\` (${occ} occurrence${occ !== 1 ? \"s\" : \"\"} modified)\\n`;\n }\n\n if (report.error) {\n res += `*Error:* ${report.error}\\n`;\n }\n if (report.warning) {\n res += `*Warning:* ${report.warning}\\n`;\n }\n\n if (report.critic_markup) {\n res += `*Preview (CriticMarkup):*\\n> ${report.critic_markup.split(\"\\\\n\").join(\"\\\\n> \")}\\n`;\n }\n if (report.clean_text) {\n res += `*Preview (Clean):*\\n> ${report.clean_text.split(\"\\\\n\").join(\"\\\\n> \")}\\n`;\n }\n res += \"\\n\";\n }\n }\n\n if (stats.skipped_details && stats.skipped_details.length > 0) {\n res += `Skipped Details:\\n${stats.skipped_details.join(\"\\n\")}`;\n }\n return res.trim();\n}\n\n// --- Startup ---\nasync function main() {\n const cliOutput = handleServerCliArgs(process.argv.slice(2), packageVersion);\n if (cliOutput !== null) {\n // stdout is safe here: the stdio transport was never started.\n process.stdout.write(cliOutput + \"\\n\");\n return;\n }\n const transport = new StdioServerTransport();\n await server.connect(transport);\n const gitSha = process.env.GIT_SHA || \"unknown\";\n const buildTs = process.env.BUILD_TIMESTAMP || \"unknown\";\n console.error(\n `Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha}@${buildTs}`,\n );\n}\n\nmain().catch(console.error);\n","import { resolve, basename } from \"node:path\";\nimport {\n DocumentObject,\n paginate,\n split_structural_appendix,\n extract_outline,\n OutlineNode,\n RegexTimeoutError,\n userFindAllMatches,\n} from \"@adeu/core\";\n\nexport interface ToolResult {\n content: { type: \"text\"; text: string }[];\n structuredContent?: any;\n isError?: boolean;\n [key: string]: unknown;\n}\n\n// Projection style markers: `**bold**` always; `_italic_` only where the\n// underscore is not intra-word (identifiers like snake_case are literal text —\n// the projection's italics markers always hug non-whitespace at a word edge).\nconst STYLE_MARKER_RE = /\\*\\*|(?<![\\w])_(?=\\S)|(?<=\\S)_(?![\\w])/g;\n\n/**\n * Renders `prefix **match** suffix` with the document's own bold/italic\n * projection markers stripped first, so the highlight cannot collide with\n * markers already present — a regex match crossing styled runs used to\n * render as `**The **Supplier** _shall provide**_` (QA 2026-07-19 v8 F-10).\n * Markers are detected over the WHOLE region (a match boundary can cut a\n * marker away from its word-edge context), then each part is rebuilt from\n * the surviving characters. Mirrors Python's _emphasized_snippet.\n */\nexport function emphasizedSnippet(\n prefix: string,\n match: string,\n suffix: string,\n): string {\n const region = prefix + match + suffix;\n const b1 = prefix.length;\n const b2 = prefix.length + match.length;\n const keep = new Array<boolean>(region.length).fill(true);\n for (const m of region.matchAll(STYLE_MARKER_RE)) {\n for (let i = m.index!; i < m.index! + m[0].length; i++) keep[i] = false;\n }\n let strippedPrefix = \"\";\n let strippedMatch = \"\";\n let strippedSuffix = \"\";\n for (let i = 0; i < region.length; i++) {\n if (!keep[i]) continue;\n if (i < b1) strippedPrefix += region[i];\n else if (i < b2) strippedMatch += region[i];\n else strippedSuffix += region[i];\n }\n return `${strippedPrefix}**${strippedMatch}**${strippedSuffix}`;\n}\n\nfunction _build_appendix_pointer(has_appendix: boolean): string {\n if (!has_appendix) return \"\";\n return `\\n\\n---\\n\\n> **Appendix available.** This document has structural metadata (defined terms, cross-references, bookmarks, diagnostics) that may be relevant when editing. Call \\`read_docx\\` with \\`mode='appendix'\\` to load it before submitting edits.`;\n}\n\nfunction _build_page_banner(page: number, total: number): string {\n if (total <= 1) return \"\";\n return `> **Page ${page} of ${total}** — call \\`read_docx\\` with \\`mode='outline'\\` for a heading map of the full document.\\n\\n---\\n\\n`;\n}\n\nfunction _build_page_footer(\n page: number,\n total: number,\n has_next: boolean,\n): string {\n if (total <= 1 || !has_next) return \"\";\n return `\\n\\n---\\n\\n> **Continues on page ${page + 1} of ${total}.**`;\n}\n\nexport function render_outline_tree(\n nodes: OutlineNode[],\n max_level: number = 2,\n verbose: boolean = false,\n): string {\n if (!nodes || nodes.length === 0) {\n return \"# (No headings detected)\\n\\nThis document has no detectable headings.\";\n }\n\n const visible = nodes.filter((n) => n.level <= max_level);\n\n if (visible.length === 0) {\n return `# (No headings at level <= ${max_level})\\n\\nDocument has ${nodes.length} headings, all at deeper levels. Call read_docx with mode='outline' and outline_max_level=N (up to 6) to see them.`;\n }\n\n const lines: string[] = [];\n for (const node of visible) {\n const prefix = \"#\".repeat(node.level);\n if (verbose) {\n const meta_parts = [`p${node.page}`, node.style];\n if (node.has_table) meta_parts.push(\"has table\");\n if (node.footnote_ids && node.footnote_ids.length > 0)\n meta_parts.push(\"fn:\" + node.footnote_ids.join(\",\"));\n lines.push(`${prefix} ${node.text} (${meta_parts.join(\", \")})`);\n } else {\n lines.push(`${prefix} ${node.text} (p${node.page})`);\n }\n }\n return lines.join(\"\\n\");\n}\n\nexport function build_full_document_response(\n text: string,\n file_path: string,\n): ToolResult {\n // The ENTIRE document body with no page banner, continuation footer, or\n // appendix pointer — the round-trip artifact for text-based apply/diff\n // (QA 2026-07-17 F1; mirrors Python's build_full_document_response).\n const [body] = split_structural_appendix(text);\n const ui_markdown = body;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: basename(file_path),\n },\n };\n}\n\nexport function build_paginated_response(\n text: string,\n page: number,\n file_path: string,\n): ToolResult {\n const [body, appendix] = split_structural_appendix(text);\n const has_appendix = Boolean(appendix.trim());\n\n const result = paginate(body, \"\");\n\n if (page < 1 || page > result.total_pages) {\n throw new Error(\n `Page ${page} out of range (doc has ${result.total_pages} pages).`,\n );\n }\n\n const selected = result.pages[page - 1];\n const banner = _build_page_banner(selected.page, selected.total_pages);\n const footer = _build_page_footer(\n selected.page,\n selected.total_pages,\n selected.has_next,\n );\n const appendix_pointer = _build_appendix_pointer(has_appendix);\n\n const ui_markdown =\n banner + selected.page_content + footer + appendix_pointer;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n // Include structuredContent for the UI to render the markdown\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: basename(file_path),\n },\n };\n}\n\nexport function build_outline_response(\n doc: DocumentObject,\n projected_text: string,\n file_path: string,\n outline_max_level: number = 2,\n outline_verbose: boolean = false,\n paragraph_offsets: Map<any, [number, number]> | null = null,\n): ToolResult {\n // Levels outside 1-6 are meaningless (0/negative would render a\n // nonsensical \"L1-L0\" range label, QA L2). Clamp to the nearest sensible\n // depth, mirroring the Python builder.\n outline_max_level = Math.max(1, Math.min(outline_max_level, 6));\n\n const [body] = split_structural_appendix(projected_text);\n const pagination_result = paginate(body, \"\");\n\n const nodes = extract_outline(\n doc,\n body,\n pagination_result.body_pages,\n pagination_result.body_page_offsets,\n paragraph_offsets,\n );\n\n const rendered = render_outline_tree(\n nodes,\n outline_max_level,\n outline_verbose,\n );\n\n const visible_count = nodes.filter(\n (n) => n.level <= outline_max_level,\n ).length;\n const deeper_count = nodes.length - visible_count;\n const deeper_hint =\n deeper_count > 0\n ? ` (${deeper_count} more at deeper levels, raise outline_max_level to see)`\n : \"\";\n\n const header = `> **Outline view** — showing ${visible_count} of ${nodes.length} headings (L1-L${outline_max_level}${deeper_hint}) across ${pagination_result.total_pages} page(s). Call \\`read_docx\\` with \\`mode='full'\\` and \\`page=N\\` to read a section.\\n\\n---\\n\\n`;\n const ui_markdown = header + rendered;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: `Outline: ${basename(file_path)}`,\n },\n };\n}\n\nexport function build_appendix_response(\n text: string,\n page: number,\n file_path: string,\n): ToolResult {\n const [, appendix] = split_structural_appendix(text);\n\n if (!appendix.trim()) {\n const ui_markdown =\n \"# Appendix\\n\\nThis document has no structural appendix (no defined terms, named anchors, or diagnostics detected).\";\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: `Appendix: ${basename(file_path)}`,\n },\n };\n }\n\n const result = paginate(appendix, \"\");\n\n if (page < 1 || page > result.total_pages) {\n throw new Error(\n `Appendix page ${page} out of range (appendix has ${result.total_pages} pages).`,\n );\n }\n\n const selected = result.pages[page - 1];\n\n let banner = \"\";\n let footer = \"\";\n\n if (selected.total_pages > 1) {\n banner = `> **Appendix page ${selected.page} of ${selected.total_pages}** — structural metadata for this document.\\n\\n---\\n\\n`;\n footer = selected.has_next\n ? `\\n\\n---\\n\\n> **Continues on appendix page ${selected.page + 1} of ${selected.total_pages}.**`\n : \"\";\n } else {\n banner =\n \"> **Appendix** — structural metadata for this document.\\n\\n---\\n\\n\";\n }\n\n const ui_markdown = banner + selected.page_content + footer;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: `Appendix: ${basename(file_path)}`,\n },\n };\n}\n\nexport function build_search_response(\n text: string,\n search_query: string,\n search_regex: boolean,\n search_case_sensitive: boolean,\n page: number | string | undefined,\n file_path: string,\n): ToolResult {\n const [body] = split_structural_appendix(text);\n const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const flags = search_case_sensitive ? \"g\" : \"gi\";\n\n // When the caller asked for a regex but supplied something the engine can't\n // compile (e.g. an unterminated character class `\\[`, or an inline-flag group\n // `(?i)...` that JS RegExp rejects), do NOT hard-error and burn the turn.\n // Downgrade to a literal search of the raw string and tell the model, so it\n // can either accept the literal hits or fix its pattern — instead of retrying\n // the same broken regex.\n let regexDowngradedNote = \"\";\n let regex: RegExp;\n let isUserRegex = false;\n if (search_regex) {\n try {\n regex = new RegExp(search_query, flags);\n isUserRegex = true;\n } catch (e: any) {\n regexDowngradedNote =\n `> **Note:** \\`${search_query}\\` is not a valid regular expression ` +\n `(${e.message}), so it was searched as literal text instead. ` +\n `If you meant a regex, fix the pattern; if you meant literal text, set \\`search_regex\\` to false.`;\n regex = new RegExp(escapeRegExp(search_query), flags);\n }\n } else {\n regex = new RegExp(escapeRegExp(search_query), flags);\n }\n\n // Patterns that blow the matching time budget (catastrophic backtracking,\n // QA 2026-07-17 F5) get the same literal downgrade as invalid patterns —\n // for a read-only search, degraded results beat a hung event loop.\n let allMatches: Array<{ 0: string; index?: number }>;\n if (isUserRegex) {\n try {\n allMatches = userFindAllMatches(search_query, body, flags).map((m) => ({\n 0: body.slice(m.start, m.end),\n index: m.start,\n }));\n } catch (e: any) {\n if (!(e instanceof RegexTimeoutError)) throw e;\n regexDowngradedNote =\n `> **Note:** \\`${search_query}\\` was searched as literal text instead of as ` +\n `a regular expression: ${e.message}`;\n allMatches = Array.from(body.matchAll(new RegExp(escapeRegExp(search_query), flags)));\n }\n } else {\n allMatches = Array.from(body.matchAll(regex));\n }\n\n // Compute document pagination once — needed for both annotation and filtering.\n const pag_res = paginate(body, \"\");\n const page_offsets = pag_res.body_page_offsets;\n const total_doc_pages = pag_res.total_pages;\n\n // Resolve `page` parameter to either \"all\" or a concrete document-page number.\n // Undefined → \"all\" (search across the whole document).\n // \"all\" (case-insensitive) → \"all\".\n // A positive integer N → filter matches to document page N.\n // Anything else → hard error.\n let filter_doc_page: number | null = null; // null means \"all\"\n if (page !== undefined && page !== null) {\n const pageStr = String(page).toLowerCase();\n if (pageStr !== \"all\") {\n const parsed = parseInt(pageStr, 10);\n if (isNaN(parsed) || parsed < 1) {\n throw new Error(\n `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.`,\n );\n }\n if (parsed > total_doc_pages) {\n throw new Error(\n `Document page ${parsed} is out of range — 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.`,\n );\n }\n filter_doc_page = parsed;\n }\n }\n\n // Helper: which document page does an offset live on?\n const pageOfOffset = (offset: number): number => {\n let p = 1;\n for (let j = 0; j < page_offsets.length; j++) {\n if (offset >= page_offsets[j]) p = j + 1;\n else break;\n }\n return p;\n };\n\n // Apply the filter (if any), but keep a record of all pages that had hits\n // so we can show a useful summary even when filtered.\n const pagesWithHits = new Set<number>();\n for (const m of allMatches) {\n pagesWithHits.add(pageOfOffset(m.index!));\n }\n\n const matches =\n filter_doc_page === null\n ? allMatches\n : allMatches.filter((m) => pageOfOffset(m.index!) === filter_doc_page);\n\n // --- Empty result ---\n if (matches.length === 0) {\n let body_msg: string;\n if (filter_doc_page !== null) {\n if (allMatches.length === 0) {\n body_msg = `> **Search Results** — No matches found for query \\`${search_query}\\` in \\`${basename(file_path)}\\`.\\n\\nVerify your search spelling, or try setting \\`search_case_sensitive\\` to false or enabling \\`search_regex\\` if you used pattern wildcards.`;\n } else {\n const hitPages = Array.from(pagesWithHits).sort((a, b) => a - b);\n body_msg = `> **Search Results** — No matches for \\`${search_query}\\` on document page ${filter_doc_page}.\\n\\nThe 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.`;\n }\n } else {\n body_msg = `> **Search Results** — No matches found for query \\`${search_query}\\` in \\`${basename(file_path)}\\`.\\n\\nVerify your search spelling, or try setting \\`search_case_sensitive\\` to false or enabling \\`search_regex\\` if you used pattern wildcards.`;\n }\n if (regexDowngradedNote) body_msg = `${regexDowngradedNote}\\n\\n${body_msg}`;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${body_msg}`;\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: body_msg,\n title: `Search: ${basename(file_path)}`,\n file_path: resolve(file_path),\n },\n };\n }\n\n // --- Build the response ---\n const ui_parts: string[] = [];\n\n if (filter_doc_page !== null) {\n ui_parts.push(\n `> **Search Results** — 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)}\\`.`,\n );\n const otherPages = Array.from(pagesWithHits)\n .filter((p) => p !== filter_doc_page)\n .sort((a, b) => a - b);\n if (otherPages.length > 0) {\n ui_parts.push(\n `> Additional matches exist on page${otherPages.length !== 1 ? \"s\" : \"\"} ${otherPages.join(\", \")} — omit \\`page\\` or pass \\`page='all'\\` to see them.`,\n );\n }\n } else {\n ui_parts.push(\n `> **Search Results** — Found ${matches.length} match${matches.length !== 1 ? \"es\" : \"\"} for \\`${search_query}\\` in \\`${basename(file_path)}\\`.`,\n );\n if (total_doc_pages > 1) {\n // Build a per-page hit distribution: \"p1: 3, p3: 1, p7: 12\"\n const counts = new Map<number, number>();\n for (const m of allMatches) {\n const p = pageOfOffset(m.index!);\n counts.set(p, (counts.get(p) || 0) + 1);\n }\n const distribution = Array.from(counts.entries())\n .sort((a, b) => a[0] - b[0])\n .map(([p, n]) => `p${p}: ${n}`)\n .join(\", \");\n ui_parts.push(\n `> Distribution across ${total_doc_pages} document pages — ${distribution}. Pass \\`page=N\\` to filter to a specific document page.`,\n );\n }\n }\n\n // Per-match occurrence counts use the FULL match set, not the filtered one —\n // this gives the LLM accurate global counts even when filtering.\n const occurrences_map: Record<string, number> = {};\n for (const m of allMatches) {\n const matched_str = m[0];\n occurrences_map[matched_str] = (occurrences_map[matched_str] || 0) + 1;\n }\n\n function get_heading(idx: number, txt: string): string {\n const txtBefore = txt.substring(0, idx);\n const lines = txtBefore.split(\"\\n\");\n const path: string[] = [];\n let current_level = 999;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n const m = line.match(/^(#{1,6})\\s+(.*)/);\n if (m) {\n const level = m[1].length;\n if (level < current_level) {\n let cleanHeading = m[2]\n .replace(/\\*\\*|__|[*_]/g, \"\")\n .replace(/\\{#[^}]+\\}/g, \"\")\n .trim();\n if (cleanHeading.length > 80) {\n cleanHeading = cleanHeading.substring(0, 80) + \"...\";\n }\n path.unshift(cleanHeading);\n current_level = level;\n if (level === 1) break;\n }\n }\n }\n return path.join(\" > \");\n }\n\n let i = 1;\n for (const m of matches) {\n const matched_str = m[0];\n const m_start = m.index!;\n const m_end = m_start + matched_str.length;\n const p_num = pageOfOffset(m_start);\n\n const snippet_start = Math.max(0, m_start - 100);\n const snippet_end = Math.min(body.length, m_end + 100);\n const snippet = emphasizedSnippet(\n body.substring(snippet_start, m_start),\n matched_str,\n body.substring(m_end, snippet_end),\n );\n\n const snippet_lines = snippet\n .split(\"\\n\")\n .filter((line) => line.trim().length > 0)\n .map((line) => `> ${line}`)\n .join(\"\\n\");\n\n ui_parts.push(\"---\");\n ui_parts.push(`### Match ${i} (p${p_num})`);\n\n const h_path = get_heading(m_start, body);\n if (h_path) {\n ui_parts.push(`**Path:** \\`${h_path}\\``);\n }\n\n const count = occurrences_map[matched_str];\n ui_parts.push(snippet_lines);\n ui_parts.push(\n `*Occurrences:* This exact phrasing appears ${count} time${count !== 1 ? \"s\" : \"\"} in the document.`,\n );\n\n i++;\n }\n\n if (regexDowngradedNote) ui_parts.unshift(regexDowngradedNote);\n const ui_markdown = ui_parts.join(\"\\n\\n\");\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n title: `Search: ${basename(file_path)}`,\n file_path: resolve(file_path),\n },\n };\n}\n","// FILE: node/packages/mcp-server/src/shared.ts\nexport const MARKDOWN_UI_URI = \"ui://adeu/markdown-ui\";\n\n/**\n * Minimal CLI handling BEFORE the stdio server starts: `--help` and\n * `--version` must print and exit like every other executable instead of\n * silently starting the transport (QA 2026-07-19 v8 F-06). Returns the text\n * to print (caller exits without serving), or null to proceed with server\n * startup. Unknown arguments are tolerated — MCP hosts append their own\n * flags. Lives here (not index.ts) so tests can import it without booting\n * the server.\n */\nexport function handleServerCliArgs(\n argv: string[],\n packageVersion: string,\n): string | null {\n if (argv.includes(\"--version\") || argv.includes(\"-v\")) {\n return `adeu-mcp-server ${packageVersion}`;\n }\n if (argv.includes(\"--help\") || argv.includes(\"-h\")) {\n return [\n \"Usage: adeu-mcp-server [options]\",\n \"\",\n \"Adeu MCP server (stdio transport, zero-dependency Node engine).\",\n \"Started by MCP hosts such as Claude Desktop; it reads JSON-RPC on stdin.\",\n \"\",\n \"Options:\",\n \" -h, --help Show this help and exit\",\n \" -v, --version Print the server version and exit\",\n \"\",\n \"Docs: https://github.com/dealfluence/adeu\",\n ].join(\"\\n\");\n }\n return null;\n}\n"],"mappings":"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","names":["basename","resolve","DocumentObject","ui_markdown","llm_content","i","packageVersion","basename","DocumentObject","res","resolve","gitSha"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/response-builders.ts","../src/shared.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { basename, resolve, extname, dirname, join } from \"node:path\";\nimport { z } from \"zod\";\nimport {\n registerAppTool as origRegisterAppTool,\n registerAppResource,\n RESOURCE_MIME_TYPE,\n} from \"@modelcontextprotocol/ext-apps/server\";\nimport fs from \"node:fs\";\nimport {\n identifyEngine,\n extractTextFromBuffer,\n _extractTextFromDoc,\n DocumentObject,\n RedlineEngine,\n BatchValidationError,\n create_word_patch_diff,\n collect_media_difference_warnings,\n finalize_document,\n} from \"@adeu/core\";\nimport { describe_illegal_control_chars } from \"@adeu/core\";\n\nimport {\n build_paginated_response,\n build_full_document_response,\n build_outline_response,\n build_appendix_response,\n build_search_response,\n} from \"./response-builders.js\";\n\nimport { MARKDOWN_UI_URI, handleServerCliArgs } from \"./shared.js\";\n// Parity with Python models.py `_infer_type_in_place` + `_coerce_match_mode_in_place`.\n// The MCP boundary schema is permissive; these repairs let recoverable payloads\n// (a missing `type` that's unambiguous from the key signature, or a non-canonical\n// `match_mode`) succeed instead of failing the whole-array Zod parse with an\n// opaque -32602. Anything still un-inferrable is caught by the handler guard\n// below and reported per-index; anything that doesn't apply to the document is\n// caught by the engine's validate_edits. Mirrors how Python repairs in a\n// BeforeValidator ahead of its (strict) discriminated union.\nconst MATCH_MODE_SYNONYMS: Record<string, \"strict\" | \"first\" | \"all\"> = {\n strict: \"strict\",\n first: \"first\",\n all: \"all\",\n first_only: \"first\",\n firstonly: \"first\",\n \"first-only\": \"first\",\n all_occurrences: \"all\",\n alloccurrences: \"all\",\n \"all-occurrences\": \"all\",\n every: \"all\",\n};\n\nfunction coerceChangeItemInPlace(item: any): void {\n if (item === null || typeof item !== \"object\" || Array.isArray(item)) return;\n\n // Infer a missing `type` ONLY when exactly one variant fits unambiguously.\n // Deliberately do NOT infer from `target_id` alone (accept vs reject is a\n // semantic choice) or `target_text` alone (delete_row vs empty-new_text\n // modify). Those stay absent and are rejected with a clear message.\n if (!(\"type\" in item) || item.type === undefined || item.type === null) {\n if (\"cells\" in item) item.type = \"insert_row\";\n else if (\"text\" in item && \"target_id\" in item) item.type = \"reply\";\n else if (\"target_text\" in item && \"new_text\" in item) item.type = \"modify\";\n }\n\n // Normalize match_mode: canonical passes through, synonyms map, anything else\n // (help-string echo \"strict, first, or all\", empty, non-string) is dropped so\n // the engine's \"strict\" default applies. Never coerce junk to \"all\" — that\n // would silently mass-edit; defaulting to strict fails safe with an\n // ambiguity error instead.\n if (\"match_mode\" in item) {\n const raw = item.match_mode;\n if (typeof raw !== \"string\") {\n delete item.match_mode;\n } else {\n const mapped = MATCH_MODE_SYNONYMS[raw.trim().toLowerCase()];\n if (mapped === undefined) delete item.match_mode;\n else item.match_mode = mapped;\n }\n }\n}\nfunction readFileBytesOrThrow(filePath: string): Buffer {\n try {\n return readFileSync(filePath);\n } catch (err: any) {\n if (err.code === \"ENOENT\") {\n // Lean, agent-appropriate error: list sibling .docx files so the model\n // can self-correct a wrong filename (e.g. a guessed `-processed` suffix)\n // in one turn, instead of being handed CLI install instructions that are\n // irrelevant inside an agent loop and pure token waste.\n let available = \"\";\n try {\n const dir = dirname(filePath);\n const docs = fs\n .readdirSync(dir)\n .filter((f) => f.toLowerCase().endsWith(\".docx\"));\n available = docs.length\n ? ` available files: [${docs.join(\", \")}]`\n : ` (no .docx files found in ${dir})`;\n } catch {\n // Directory unreadable — omit the listing rather than fail.\n }\n throw new Error(`file not found: ${basename(filePath)};${available}`);\n }\n throw err;\n }\n}\n\n// --- Asset Loaders for UI ---\nconst DIST_DIR = import.meta.dirname;\n\nfunction getAssetContent(\n folder: \"templates\" | \"assets\",\n filename: string,\n fallbackMessage: string,\n): string {\n const filePath = join(DIST_DIR, folder, filename);\n if (existsSync(filePath)) {\n return readFileSync(filePath, \"utf-8\");\n }\n return fallbackMessage;\n}\n\n// --- Tool Description Constants ---\nconst READ_DOCX_COMMON_DESC =\n \"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\";\nconst READ_DOCX_TAIL =\n \"Modes:\\n- 'full' (default): paginated body content. Use page=N to navigate.\\n- 'outline': heading map only — 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.\";\n\nconst PROCESS_BATCH_COMMON_DESC =\n \"Applies a batch of edits and review actions to a DOCX.\\n\\nBatches apply SEQUENTIALLY: each change is validated and applied against the document state produced by the changes before it, so you may chain dependent edits within one batch (e.g. rename X to Y, then modify Y — the second edit must target Y, the text as it reads after the rename). Validation failures reject the whole batch transactionally: nothing is applied until every change resolves.\\n\\n\";\nconst PROCESS_BATCH_OPERATIONS_DESC =\n \"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.\\n • EMPTY/FORM TABLE CELLS: a blank cell has no text to match. `read_docx` renders each cell with a trailing `{#cell:<id>}` anchor — 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 — 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. 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.\";\n\nconst DIFF_DOCX_DESC =\n \"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.\";\n\nconst gitSha = process.env.GIT_SHA || \"unknown\";\nconst packageVersion = process.env.PACKAGE_VERSION || \"unknown\";\nconst buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;\n\n// --- Server Setup ---\nconst server = new McpServer({\n name: \"adeu-redlining-service\",\n version: packageVersion,\n});\n\n// Wrap server.registerTool to inject buildTag into descriptions\nconst originalRegisterTool = server.registerTool.bind(server);\nserver.registerTool = (name: string, schema: any, handler?: any) => {\n if (schema && typeof schema === \"object\") {\n // Idempotent: UI tools route through BOTH this wrapper and the\n // registerAppTool wrapper, so guard against stamping the tag twice.\n if (schema.description && !schema.description.includes(buildTag.trim())) {\n schema.description = schema.description.trim() + buildTag;\n }\n }\n return originalRegisterTool(name, schema, handler);\n};\n\n// Wrap registerAppTool to inject buildTag into descriptions\nconst registerAppTool: typeof origRegisterAppTool = (\n mcpServer,\n name,\n schema,\n handler,\n) => {\n if (schema && typeof schema === \"object\") {\n if (schema.description) {\n schema.description = schema.description.trim() + buildTag;\n }\n }\n return origRegisterAppTool(mcpServer, name, schema, handler);\n};\n\n// Common CSP allowing Google Fonts used by Adeu UI templates\nconst UI_CSP = {\n connectDomains: [\"https://fonts.googleapis.com\", \"https://fonts.gstatic.com\"],\n resourceDomains: [\n \"https://fonts.googleapis.com\",\n \"https://fonts.gstatic.com\",\n ],\n};\n\n// ==========================================\n// 1. UI RESOURCES\n// ==========================================\n\nregisterAppResource(\n server,\n MARKDOWN_UI_URI,\n MARKDOWN_UI_URI,\n { mimeType: RESOURCE_MIME_TYPE, description: \"Adeu Markdown Viewer UI\" },\n async () => {\n let html = getAssetContent(\n \"templates\",\n \"markdown_ui.html\",\n \"<html><body>UI Template Not Found</body></html>\",\n );\n const markedJs = getAssetContent(\n \"assets\",\n \"marked.min.js\",\n \"window.__MARKED_ERROR = 'marked.min.js not found';\",\n );\n const svg = getAssetContent(\"assets\", \"adeu.svg\", \"\");\n\n html = html\n .replace(\"[[marked_js_code | safe]]\", markedJs)\n .replace(\"[[ adeu_svg_code ]]\", svg);\n\n return {\n contents: [\n {\n uri: MARKDOWN_UI_URI,\n mimeType: RESOURCE_MIME_TYPE,\n text: html,\n _meta: { ui: { csp: UI_CSP } },\n },\n ],\n };\n },\n);\n\n// ==========================================\n// 2. UI-ENABLED TOOLS\n// ==========================================\nregisterAppTool(\n server,\n \"read_docx\",\n {\n title: \"Read DOCX\",\n description: READ_DOCX_COMMON_DESC + READ_DOCX_TAIL,\n inputSchema: z.object({\n reasoning: z\n .string()\n .describe(\n \"Why do I need to read this docx document? State this reason before any other parameter.\",\n ),\n file_path: z.string().describe(\"Absolute path to the DOCX file.\"),\n clean_view: z\n .boolean()\n .default(false)\n .describe(\n \"If False (default), returns the 'Raw' text with inline CriticMarkup. If True, returns 'Accepted' text.\",\n ),\n mode: z\n .enum([\"full\", \"outline\", \"appendix\"])\n .default(\"full\")\n .describe(\n \"'full' returns body content. 'outline' returns a structural heading map. 'appendix' returns defined terms.\",\n ),\n page: z\n .union([z.number(), z.string()])\n .optional()\n .describe(\n \"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). Note: pages are synthetic, length-based content chunks sized for LLM consumption — they do NOT correspond to printed Word pages or explicit page breaks.\",\n ),\n outline_max_level: z.coerce\n .number()\n .default(2)\n .describe(\"For mode='outline' only: cap on heading depth.\"),\n outline_verbose: z\n .boolean()\n .default(false)\n .describe(\"For mode='outline' only: includes metadata.\"),\n search_query: z\n .string()\n .optional()\n .describe(\n \"The substring or regex pattern to search for. When provided, filters results to matching paragraphs.\",\n ),\n search_regex: z\n .boolean()\n .default(false)\n .describe(\n \"Set to true to interpret search_query as a regular expression.\",\n ),\n search_case_sensitive: z\n .boolean()\n .default(true)\n .describe(\"Set to false to perform case-insensitive matching.\"),\n }),\n _meta: { ui: { resourceUri: MARKDOWN_UI_URI } },\n },\n async ({\n reasoning,\n file_path,\n clean_view,\n mode,\n page,\n outline_max_level,\n outline_verbose,\n search_query,\n search_regex,\n search_case_sensitive,\n }) => {\n try {\n void reasoning;\n const buf = readFileBytesOrThrow(file_path);\n\n if (mode === \"outline\") {\n const doc = await DocumentObject.load(buf);\n const extract_res = _extractTextFromDoc(\n doc,\n clean_view,\n true,\n true,\n ) as {\n text: string;\n paragraph_offsets: Map<any, [number, number]>;\n };\n const res = build_outline_response(\n doc,\n extract_res.text,\n file_path,\n outline_max_level,\n outline_verbose,\n extract_res.paragraph_offsets,\n );\n return res as any;\n }\n\n const text = await extractTextFromBuffer(buf, clean_view);\n if (search_query !== undefined && search_query !== null) {\n // In search mode, undefined `page` means \"search all document pages\".\n const res = build_search_response(\n text,\n search_query,\n search_regex,\n search_case_sensitive,\n page,\n file_path,\n );\n return res as any;\n }\n // In full mode, page='all' returns the entire document without page\n // chrome — the round-trip artifact for text-based apply/diff\n // (QA 2026-07-17 F1 parity with the Python CLI's --page all).\n if (\n mode === \"full\" &&\n page !== undefined &&\n page !== null &&\n String(page).trim().toLowerCase() === \"all\"\n ) {\n const res = build_full_document_response(text, file_path);\n return res as any;\n }\n // In non-search mode, `page` defaults to 1 (show document page 1).\n // Non-numeric values must error, not silently fall back to page 1\n // (QA L1 parity with the Python CLI).\n let resolvedPage = 1;\n if (page !== undefined && page !== null) {\n const parsed =\n typeof page === \"number\" ? page : parseInt(String(page).trim(), 10);\n if (!Number.isFinite(parsed)) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text:\n `Invalid page value: '${page}'. Provide a positive integer ` +\n `(pages are 1-indexed; 'all' is valid for mode='full' and together with search_query).`,\n },\n ],\n };\n }\n resolvedPage = parsed;\n }\n if (mode === \"appendix\") {\n const res = build_appendix_response(text, resolvedPage, file_path);\n return res as any;\n }\n const res = build_paginated_response(text, resolvedPage, file_path);\n return res as any;\n } catch (e: any) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Error executing tool read_docx: ${e.message}`,\n },\n ],\n };\n }\n },\n);\n\n// ==========================================\n// 3. HEADLESS TOOLS (No UI)\n// ==========================================\n\n// Typed shape for a single `process_document_batch` change. This makes the six\n// DocumentChange variants — and the modify-only `match_mode`/`regex` options —\n// discoverable from the tool schema itself, instead of prose alone. A bare\n// string is still accepted (and normalized in-handler) so double-serialized\n// payloads from some LLM clients keep working; only `type` is required, all\n// other fields are optional, and unknown keys pass through untouched.\nconst CHANGE_ITEM_SCHEMA = z\n .object({\n type: z\n .enum([\"modify\", \"accept\", \"reject\", \"reply\", \"insert_row\", \"delete_row\"])\n .optional()\n .describe(\n \"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.\",\n ),\n target_text: z\n .string()\n .optional()\n .describe(\n \"modify / insert_row / delete_row: the existing text to locate (interpreted as a regex when regex=true).\",\n ),\n new_text: z\n .string()\n .optional()\n .describe(\n \"modify: replacement text. Supports Markdown (headings, **bold**, _italic_, '\\\\n\\\\n' paragraph splits); empty string deletes. Regex capture groups are available as $1, $2…\",\n ),\n target_id: z\n .string()\n .optional()\n .describe(\n \"accept / reject / reply: the 'Chg:N' or 'Com:N' id taken from a fresh read_docx.\",\n ),\n text: z.string().optional().describe(\"reply: the reply body.\"),\n comment: z\n .string()\n .optional()\n .describe(\n \"modify / accept / reject: attach a margin comment to the change (no manual CriticMarkup).\",\n ),\n match_mode: z\n .enum([\"strict\", \"first\", \"all\"])\n .optional()\n .describe(\n \"modify only: 'strict' (default — target must match uniquely), 'first' (first occurrence), or 'all' (every occurrence).\",\n ),\n regex: z\n .boolean()\n .optional()\n .describe(\n \"modify only: treat target_text as a regular expression (default false).\",\n ),\n position: z\n .enum([\"above\", \"below\"])\n .optional()\n .describe(\n \"insert_row: place the new row above or below the matched row.\",\n ),\n cells: z\n .array(z.string())\n .optional()\n .describe(\"insert_row: the cell values for the new row, left to right.\"),\n })\n .passthrough();\n\nserver.registerTool(\n \"process_document_batch\",\n {\n description: PROCESS_BATCH_COMMON_DESC + PROCESS_BATCH_OPERATIONS_DESC,\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to apply these changes to the document? State this reason before any other parameter.\",\n ),\n original_docx_path: z\n .string()\n .describe(\"Absolute path to the source file.\"),\n author_name: z\n .string()\n .describe(\"Name to appear in Track Changes (e.g., 'Reviewer AI').\"),\n // Deliberately a plain REQUIRED array of typed items. Wrapping this in\n // z.preprocess (to also accept the whole array as one JSON string) drops\n // it out of the schema's `required` list, and a z.union publishes an\n // anyOf that hides the item schema — both cost more, on every call, than\n // they buy for the rare client that stringifies its payload. That client\n // gets a clear \"expected array, received string\" it can retry from.\n // Per-item stringification is still tolerated, below and in the engine.\n changes: z\n .array(z.union([z.string(), CHANGE_ITEM_SCHEMA]))\n .describe(\n \"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). Items apply SEQUENTIALLY: each one evaluates against the document state produced by the items before it, so later items may target text an earlier item introduced.\",\n ),\n output_path: z.string().optional().describe(\"Optional output path.\"),\n dry_run: z\n .boolean()\n .optional()\n .default(false)\n .describe(\n \"If True, simulates the changes and returns a detailed preview report without modifying any files.\",\n ),\n },\n },\n async ({\n reasoning,\n original_docx_path,\n author_name,\n changes,\n output_path,\n dry_run,\n }) => {\n try {\n void reasoning;\n if (!author_name || !author_name.trim())\n return {\n content: [\n { type: \"text\", text: \"Error: author_name cannot be empty.\" },\n ],\n };\n const author_ctrl = describe_illegal_control_chars(author_name);\n if (author_ctrl)\n return {\n content: [\n {\n type: \"text\",\n text:\n `Error: author_name contains control character(s) (${author_ctrl}) ` +\n `that cannot be stored in a DOCX. Remove them and retry.`,\n },\n ],\n };\n\n if (!changes || changes.length === 0)\n return {\n content: [{ type: \"text\", text: \"Error: No changes provided.\" }],\n };\n\n // Defensive sanitization at the MCP boundary: some LLM clients\n // \"double-serialize\" nested arrays, delivering each element of `changes`\n // as a JSON string instead of an object. The core engine also guards\n // against this, but we normalize here too so the tool layer never hands\n // raw string primitives downstream regardless of the engine version\n // bundled. Genuine objects and unparseable strings pass through\n // untouched so validation surfaces a clear error rather than crashing.\n const sanitizedChanges = changes.map((item: any) => {\n let obj: any = item;\n if (typeof item === \"string\") {\n try {\n const parsed = JSON.parse(item);\n obj = parsed !== null && typeof parsed === \"object\" ? parsed : item;\n } catch {\n obj = item;\n }\n }\n // Repair recoverable payloads (infer type, normalize match_mode) the\n // same way Python does before its union validation.\n if (obj !== null && typeof obj === \"object\" && !Array.isArray(obj)) {\n coerceChangeItemInPlace(obj);\n }\n return obj;\n });\n\n // Boundary guard, scoped narrowly: after inference, reject only an OBJECT\n // that still carries no resolvable `type`. Strings, nulls, and non-objects\n // are intentionally left for the engine's validate_edits to report\n // (\"Invalid change format… received a primitive\"), keeping the engine the\n // single authority for those and avoiding a competing error surface.\n // A typeless object is the one case the engine can't cleanly reject (with\n // `type` now optional it would fall into the edits bucket as a no-op), so\n // it is caught here with an actionable, per-index message.\n const VALID_TYPES = new Set([\n \"modify\",\n \"accept\",\n \"reject\",\n \"reply\",\n \"insert_row\",\n \"delete_row\",\n ]);\n const typeErrors: string[] = [];\n sanitizedChanges.forEach((c: any, i: number) => {\n if (\n c !== null &&\n typeof c === \"object\" &&\n !Array.isArray(c) &&\n (!c.type || !VALID_TYPES.has(c.type))\n ) {\n typeErrors.push(\n `- 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(\", \")}].`,\n );\n }\n });\n if (typeErrors.length > 0) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Batch rejected. Some changes are malformed:\\n\\n${typeErrors.join(\"\\n\")}`,\n },\n ],\n };\n }\n\n let outPath = output_path;\n if (!outPath) {\n const ext = extname(original_docx_path);\n const base = basename(original_docx_path, ext);\n const dir = dirname(original_docx_path);\n // Idempotency guard (parity with Python document.py): if the input is\n // already a processed artifact, write back to it instead of compounding\n // the suffix into contract_processed_processed.docx, which fragments the\n // agent's document state across files.\n if (base.endsWith(\"_processed\") || base.endsWith(\"_redlined\")) {\n outPath = resolve(dir, `${base}${ext}`);\n } else {\n outPath = resolve(dir, `${base}_processed${ext}`);\n }\n }\n\n const buf = readFileBytesOrThrow(original_docx_path);\n const doc = await DocumentObject.load(buf);\n const engine = new RedlineEngine(doc, author_name);\n\n let stats;\n try {\n stats = engine.process_batch(sanitizedChanges, dry_run);\n } catch (e: any) {\n if (e instanceof BatchValidationError) {\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Batch rejected. Some edits failed validation:\\n\\n${e.errors.join(\"\\n\\n\")}`,\n },\n ],\n };\n }\n throw e;\n }\n\n if (!dry_run) {\n const outBuf = await doc.save();\n try {\n fs.writeFileSync(outPath, outBuf);\n } catch (e: any) {\n // Filesystem failures (name too long, missing directory, perms)\n // must surface as a clear, actionable error (QA H3 parity).\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: `Could not write output file '${outPath}': ${e.message}`,\n },\n ],\n };\n }\n }\n\n let res = formatBatchResult(stats, outPath, !!dry_run);\n if (sanitizedChanges.length === 0) {\n res =\n `⚠️ 0 changes provided — nothing to do. The output is an unmodified copy of the original.\\n\\n` +\n res;\n }\n return { content: [{ type: \"text\", text: res }] };\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\nserver.registerTool(\n \"accept_all_changes\",\n {\n description:\n \"Accepts all tracked changes and removes all comments in a single operation.\",\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to accept all changes in this document? State this reason before any other parameter.\",\n ),\n docx_path: z.string().describe(\"Absolute path to the DOCX file.\"),\n output_path: z.string().optional().describe(\"Optional output path.\"),\n },\n },\n async ({ reasoning, docx_path, output_path }) => {\n try {\n void reasoning;\n let outPath = output_path;\n if (!outPath) {\n const ext = extname(docx_path);\n const base = basename(docx_path, ext);\n const dir = dirname(docx_path);\n outPath = resolve(dir, `${base}_clean${ext}`);\n }\n\n const buf = readFileBytesOrThrow(docx_path);\n const doc = await DocumentObject.load(buf);\n const engine = new RedlineEngine(doc);\n\n engine.accept_all_revisions();\n\n const outBuf = await doc.save();\n\n fs.writeFileSync(outPath, outBuf);\n\n return {\n content: [\n { type: \"text\", text: `Accepted all changes. Saved to: ${outPath}` },\n ],\n };\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\nserver.registerTool(\n \"diff_docx_files\",\n {\n description: DIFF_DOCX_DESC,\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to diff these two documents? State this reason before any other parameter.\",\n ),\n original_path: z\n .string()\n .describe(\"Absolute path to the baseline DOCX file.\"),\n modified_path: z\n .string()\n .describe(\"Absolute path to the modified DOCX file.\"),\n compare_clean: z\n .boolean()\n .default(true)\n .describe(\n \"If True, compares 'Accepted' state. If False, compares raw text.\",\n ),\n },\n },\n async ({ reasoning, original_path, modified_path, compare_clean }) => {\n try {\n void reasoning;\n const origBuf = readFileBytesOrThrow(original_path);\n const modBuf = readFileBytesOrThrow(modified_path);\n\n // includeAppendix=false: the generated appendix (\"used N times\",\n // diagnostics) is not document content — diffing it produces phantom\n // changes no apply can consume (QA 2026-07-18 H1).\n const origText = await extractTextFromBuffer(origBuf, compare_clean, false);\n const modText = await extractTextFromBuffer(modBuf, compare_clean, false);\n\n const diff = create_word_patch_diff(\n origText,\n modText,\n basename(original_path),\n basename(modified_path),\n );\n\n // A text diff cannot see image bytes: when embedded media differ, an\n // empty diff must never read as \"the documents are identical\"\n // (QA 2026-07-19 F-04).\n const media_warnings = collect_media_difference_warnings(\n new Uint8Array(origBuf),\n new Uint8Array(modBuf),\n );\n const warning_text = media_warnings.length\n ? media_warnings.map((w) => `⚠️ ${w}`).join(\"\\n\") + \"\\n\\n\"\n : \"\";\n\n return {\n content: [\n {\n type: \"text\",\n text: warning_text + (diff || \"No differences found.\"),\n },\n ],\n };\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\nserver.registerTool(\n \"finalize_document\",\n {\n description:\n \"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.\",\n inputSchema: {\n reasoning: z\n .string()\n .describe(\n \"Why do I need to finalize this document? State this reason before any other parameter.\",\n ),\n file_path: z.string().describe(\"Absolute path to the DOCX file.\"),\n output_path: z.string().optional().describe(\"Optional output path.\"),\n sanitize_mode: z\n .enum([\"full\", \"keep-markup\"])\n .optional()\n .describe(\"full removes all markup, keep-markup redacts metadata.\"),\n accept_all: z\n .boolean()\n .optional()\n .describe(\n \"If true, auto-accepts all unresolved track changes before finalizing.\",\n ),\n protection_mode: z\n .enum([\"read_only\", \"encrypt\"])\n .optional()\n .describe(\n \"Native OOXML document locking. Note: 'encrypt' is unsupported in this zero-dependency build and falls back to 'read_only'.\",\n ),\n password: z.string().optional().describe(\"Ignored in this environment.\"),\n author: z\n .string()\n .optional()\n .describe(\"Replace all remaining markup authorship with this name.\"),\n export_pdf: z\n .boolean()\n .optional()\n .describe(\"Ignored in this environment.\"),\n },\n },\n async ({\n reasoning,\n file_path,\n output_path,\n sanitize_mode,\n accept_all,\n protection_mode,\n author,\n export_pdf,\n }) => {\n try {\n void reasoning;\n let outPath = output_path;\n if (!outPath) {\n const ext = extname(file_path);\n const base = basename(file_path, ext);\n const dir = dirname(file_path);\n outPath = resolve(dir, `${base}_final${ext}`);\n }\n\n const buf = readFileBytesOrThrow(file_path);\n const doc = await DocumentObject.load(buf);\n\n const result = await finalize_document(doc, {\n filename: basename(file_path),\n sanitize_mode: (sanitize_mode as any) || \"full\",\n accept_all: accept_all as boolean,\n protection_mode: protection_mode as any,\n author: author as string,\n export_pdf: export_pdf as boolean,\n });\n\n if (result.outBuffer) {\n fs.writeFileSync(outPath, result.outBuffer);\n return {\n content: [\n {\n type: \"text\",\n text: `Saved to: ${outPath}\\n\\n${result.reportText}`,\n },\n ],\n };\n } else {\n return {\n content: [\n {\n type: \"text\",\n text: result.reportText,\n },\n ],\n };\n }\n } catch (e: any) {\n return {\n isError: true,\n content: [{ type: \"text\", text: `Error: ${e.message}` }],\n };\n }\n },\n);\n\n// --- Formatter for process_document_batch ---\nexport function formatBatchResult(\n stats: any,\n outPath: string,\n dry_run: boolean,\n): string {\n let res = \"\";\n if (dry_run) {\n res = `Dry-run simulation complete.\\n`;\n } else {\n res = `Batch complete. Saved to: ${outPath}\\n`;\n }\n const total_occurrences = stats.edits\n ? stats.edits.reduce(\n (acc: number, e: any) =>\n acc + (e.status === \"applied\" ? e.occurrences_modified || 1 : 0),\n 0,\n )\n : 0;\n const occ_text =\n total_occurrences > stats.edits_applied\n ? ` (${total_occurrences} occurrences)`\n : \"\";\n\n const already = stats.actions_already_resolved || 0;\n const already_text = already\n ? `, ${already} already resolved (no effect)`\n : \"\";\n res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped${already_text}.\\n`;\n res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.\\n`;\n\n if (stats.edits && stats.edits.length > 0) {\n res += \"\\nDetailed Edit Reports:\\n\";\n for (let i = 0; i < stats.edits.length; i++) {\n const report = stats.edits[i];\n const status_indicator =\n report.status === \"applied\" ? \"✅ [applied]\" : \"❌ [failed]\";\n\n const pagesStr =\n report.pages && report.pages.length > 0\n ? ` (p${report.pages.join(\", p\")})`\n : \"\";\n\n res += `### Edit ${i + 1} ${status_indicator}${pagesStr}\\n`;\n\n if (report.heading_path) {\n res += `**Path:** \\`${report.heading_path}\\`\\n`;\n }\n\n if (report.match_mode) {\n const occ =\n report.occurrences_modified || (report.status === \"applied\" ? 1 : 0);\n res += `**Mode:** \\`${report.match_mode}\\` (${occ} occurrence${occ !== 1 ? \"s\" : \"\"} modified)\\n`;\n }\n\n if (report.error) {\n res += `*Error:* ${report.error}\\n`;\n }\n if (report.warning) {\n res += `*Warning:* ${report.warning}\\n`;\n }\n\n if (report.critic_markup) {\n res += `*Preview (CriticMarkup):*\\n> ${report.critic_markup.split(\"\\\\n\").join(\"\\\\n> \")}\\n`;\n }\n if (report.clean_text) {\n res += `*Preview (Clean):*\\n> ${report.clean_text.split(\"\\\\n\").join(\"\\\\n> \")}\\n`;\n }\n res += \"\\n\";\n }\n }\n\n if (stats.skipped_details && stats.skipped_details.length > 0) {\n res += `Skipped Details:\\n${stats.skipped_details.join(\"\\n\")}`;\n }\n return res.trim();\n}\n\n// --- Startup ---\nasync function main() {\n const cliOutput = handleServerCliArgs(process.argv.slice(2), packageVersion);\n if (cliOutput !== null) {\n // stdout is safe here: the stdio transport was never started.\n process.stdout.write(cliOutput + \"\\n\");\n return;\n }\n const transport = new StdioServerTransport();\n await server.connect(transport);\n const gitSha = process.env.GIT_SHA || \"unknown\";\n const buildTs = process.env.BUILD_TIMESTAMP || \"unknown\";\n console.error(\n `Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha}@${buildTs}`,\n );\n}\n\nmain().catch(console.error);\n","import { resolve, basename } from \"node:path\";\nimport {\n DocumentObject,\n paginate,\n split_structural_appendix,\n extract_outline,\n OutlineNode,\n RegexTimeoutError,\n userFindAllMatches,\n} from \"@adeu/core\";\n\nexport interface ToolResult {\n content: { type: \"text\"; text: string }[];\n structuredContent?: any;\n isError?: boolean;\n [key: string]: unknown;\n}\n\n// Projection style markers: `**bold**` always; `_italic_` only where the\n// underscore is not intra-word (identifiers like snake_case are literal text —\n// the projection's italics markers always hug non-whitespace at a word edge).\nconst STYLE_MARKER_RE = /\\*\\*|(?<![\\w])_(?=\\S)|(?<=\\S)_(?![\\w])/g;\n\n/**\n * Renders `prefix **match** suffix` with the document's own bold/italic\n * projection markers stripped first, so the highlight cannot collide with\n * markers already present — a regex match crossing styled runs used to\n * render as `**The **Supplier** _shall provide**_` (QA 2026-07-19 v8 F-10).\n * Markers are detected over the WHOLE region (a match boundary can cut a\n * marker away from its word-edge context), then each part is rebuilt from\n * the surviving characters. Mirrors Python's _emphasized_snippet.\n */\nexport function emphasizedSnippet(\n prefix: string,\n match: string,\n suffix: string,\n): string {\n const region = prefix + match + suffix;\n const b1 = prefix.length;\n const b2 = prefix.length + match.length;\n const keep = new Array<boolean>(region.length).fill(true);\n for (const m of region.matchAll(STYLE_MARKER_RE)) {\n for (let i = m.index!; i < m.index! + m[0].length; i++) keep[i] = false;\n }\n let strippedPrefix = \"\";\n let strippedMatch = \"\";\n let strippedSuffix = \"\";\n for (let i = 0; i < region.length; i++) {\n if (!keep[i]) continue;\n if (i < b1) strippedPrefix += region[i];\n else if (i < b2) strippedMatch += region[i];\n else strippedSuffix += region[i];\n }\n return `${strippedPrefix}**${strippedMatch}**${strippedSuffix}`;\n}\n\nfunction _build_appendix_pointer(has_appendix: boolean): string {\n if (!has_appendix) return \"\";\n return `\\n\\n---\\n\\n> **Appendix available.** This document has structural metadata (defined terms, cross-references, bookmarks, diagnostics) that may be relevant when editing. Call \\`read_docx\\` with \\`mode='appendix'\\` to load it before submitting edits.`;\n}\n\nfunction _build_page_banner(page: number, total: number): string {\n if (total <= 1) return \"\";\n // \"synthetic\" is load-bearing: Adeu pages are length-based content chunks\n // sized for LLM consumption, and readers must never mistake them for\n // printed Word pages or explicit page breaks (QA 2026-07-19 ADEU-QA-005).\n return `> **Page ${page} of ${total}** (synthetic page — a length-based chunk, not a printed Word page) — call \\`read_docx\\` with \\`mode='outline'\\` for a heading map of the full document.\\n\\n---\\n\\n`;\n}\n\nfunction _build_page_footer(\n page: number,\n total: number,\n has_next: boolean,\n): string {\n if (total <= 1 || !has_next) return \"\";\n return `\\n\\n---\\n\\n> **Continues on page ${page + 1} of ${total}.**`;\n}\n\nexport function render_outline_tree(\n nodes: OutlineNode[],\n max_level: number = 2,\n verbose: boolean = false,\n): string {\n if (!nodes || nodes.length === 0) {\n return \"# (No headings detected)\\n\\nThis document has no detectable headings.\";\n }\n\n const visible = nodes.filter((n) => n.level <= max_level);\n\n if (visible.length === 0) {\n return `# (No headings at level <= ${max_level})\\n\\nDocument has ${nodes.length} headings, all at deeper levels. Call read_docx with mode='outline' and outline_max_level=N (up to 6) to see them.`;\n }\n\n const lines: string[] = [];\n for (const node of visible) {\n const prefix = \"#\".repeat(node.level);\n if (verbose) {\n const meta_parts = [`p${node.page}`, node.style];\n if (node.has_table) meta_parts.push(\"has table\");\n if (node.footnote_ids && node.footnote_ids.length > 0)\n meta_parts.push(\"fn:\" + node.footnote_ids.join(\",\"));\n lines.push(`${prefix} ${node.text} (${meta_parts.join(\", \")})`);\n } else {\n lines.push(`${prefix} ${node.text} (p${node.page})`);\n }\n }\n return lines.join(\"\\n\");\n}\n\nexport function build_full_document_response(\n text: string,\n file_path: string,\n): ToolResult {\n // The ENTIRE document body with no page banner, continuation footer, or\n // appendix pointer — the round-trip artifact for text-based apply/diff\n // (QA 2026-07-17 F1; mirrors Python's build_full_document_response).\n const [body] = split_structural_appendix(text);\n const ui_markdown = body;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: basename(file_path),\n },\n };\n}\n\nexport function build_paginated_response(\n text: string,\n page: number,\n file_path: string,\n): ToolResult {\n const [body, appendix] = split_structural_appendix(text);\n const has_appendix = Boolean(appendix.trim());\n\n const result = paginate(body, \"\");\n\n if (page < 1 || page > result.total_pages) {\n throw new Error(\n `Page ${page} out of range (doc has ${result.total_pages} pages).`,\n );\n }\n\n const selected = result.pages[page - 1];\n const banner = _build_page_banner(selected.page, selected.total_pages);\n const footer = _build_page_footer(\n selected.page,\n selected.total_pages,\n selected.has_next,\n );\n const appendix_pointer = _build_appendix_pointer(has_appendix);\n\n const ui_markdown =\n banner + selected.page_content + footer + appendix_pointer;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n // Include structuredContent for the UI to render the markdown\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: basename(file_path),\n },\n };\n}\n\nexport function build_outline_response(\n doc: DocumentObject,\n projected_text: string,\n file_path: string,\n outline_max_level: number = 2,\n outline_verbose: boolean = false,\n paragraph_offsets: Map<any, [number, number]> | null = null,\n): ToolResult {\n // Levels outside 1-6 are meaningless (0/negative would render a\n // nonsensical \"L1-L0\" range label, QA L2). Clamp to the nearest sensible\n // depth, mirroring the Python builder.\n outline_max_level = Math.max(1, Math.min(outline_max_level, 6));\n\n const [body] = split_structural_appendix(projected_text);\n const pagination_result = paginate(body, \"\");\n\n const nodes = extract_outline(\n doc,\n body,\n pagination_result.body_pages,\n pagination_result.body_page_offsets,\n paragraph_offsets,\n );\n\n const rendered = render_outline_tree(\n nodes,\n outline_max_level,\n outline_verbose,\n );\n\n const visible_count = nodes.filter(\n (n) => n.level <= outline_max_level,\n ).length;\n const deeper_count = nodes.length - visible_count;\n const deeper_hint =\n deeper_count > 0\n ? ` (${deeper_count} more at deeper levels, raise outline_max_level to see)`\n : \"\";\n\n const header = `> **Outline view** — showing ${visible_count} of ${nodes.length} headings (L1-L${outline_max_level}${deeper_hint}) across ${pagination_result.total_pages} page(s). Call \\`read_docx\\` with \\`mode='full'\\` and \\`page=N\\` to read a section.\\n\\n---\\n\\n`;\n const ui_markdown = header + rendered;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: `Outline: ${basename(file_path)}`,\n },\n };\n}\n\nexport function build_appendix_response(\n text: string,\n page: number,\n file_path: string,\n): ToolResult {\n const [, appendix] = split_structural_appendix(text);\n\n if (!appendix.trim()) {\n const ui_markdown =\n \"# Appendix\\n\\nThis document has no structural appendix (no defined terms, named anchors, or diagnostics detected).\";\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: `Appendix: ${basename(file_path)}`,\n },\n };\n }\n\n const result = paginate(appendix, \"\");\n\n if (page < 1 || page > result.total_pages) {\n throw new Error(\n `Appendix page ${page} out of range (appendix has ${result.total_pages} pages).`,\n );\n }\n\n const selected = result.pages[page - 1];\n\n let banner = \"\";\n let footer = \"\";\n\n if (selected.total_pages > 1) {\n banner = `> **Appendix page ${selected.page} of ${selected.total_pages}** — structural metadata for this document.\\n\\n---\\n\\n`;\n footer = selected.has_next\n ? `\\n\\n---\\n\\n> **Continues on appendix page ${selected.page + 1} of ${selected.total_pages}.**`\n : \"\";\n } else {\n banner =\n \"> **Appendix** — structural metadata for this document.\\n\\n---\\n\\n\";\n }\n\n const ui_markdown = banner + selected.page_content + footer;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n file_path: resolve(file_path),\n title: `Appendix: ${basename(file_path)}`,\n },\n };\n}\n\nexport function build_search_response(\n text: string,\n search_query: string,\n search_regex: boolean,\n search_case_sensitive: boolean,\n page: number | string | undefined,\n file_path: string,\n): ToolResult {\n const [body] = split_structural_appendix(text);\n const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const flags = search_case_sensitive ? \"g\" : \"gi\";\n\n // When the caller asked for a regex but supplied something the engine can't\n // compile (e.g. an unterminated character class `\\[`, or an inline-flag group\n // `(?i)...` that JS RegExp rejects), do NOT hard-error and burn the turn.\n // Downgrade to a literal search of the raw string and tell the model, so it\n // can either accept the literal hits or fix its pattern — instead of retrying\n // the same broken regex.\n let regexDowngradedNote = \"\";\n let regex: RegExp;\n let isUserRegex = false;\n if (search_regex) {\n try {\n regex = new RegExp(search_query, flags);\n isUserRegex = true;\n } catch (e: any) {\n regexDowngradedNote =\n `> **Note:** \\`${search_query}\\` is not a valid regular expression ` +\n `(${e.message}), so it was searched as literal text instead. ` +\n `If you meant a regex, fix the pattern; if you meant literal text, set \\`search_regex\\` to false.`;\n regex = new RegExp(escapeRegExp(search_query), flags);\n }\n } else {\n regex = new RegExp(escapeRegExp(search_query), flags);\n }\n\n // Patterns that blow the matching time budget (catastrophic backtracking,\n // QA 2026-07-17 F5) get the same literal downgrade as invalid patterns —\n // for a read-only search, degraded results beat a hung event loop.\n let allMatches: Array<{ 0: string; index?: number }>;\n if (isUserRegex) {\n try {\n allMatches = userFindAllMatches(search_query, body, flags).map((m) => ({\n 0: body.slice(m.start, m.end),\n index: m.start,\n }));\n } catch (e: any) {\n if (!(e instanceof RegexTimeoutError)) throw e;\n regexDowngradedNote =\n `> **Note:** \\`${search_query}\\` was searched as literal text instead of as ` +\n `a regular expression: ${e.message}`;\n allMatches = Array.from(body.matchAll(new RegExp(escapeRegExp(search_query), flags)));\n }\n } else {\n allMatches = Array.from(body.matchAll(regex));\n }\n\n // Compute document pagination once — needed for both annotation and filtering.\n const pag_res = paginate(body, \"\");\n const page_offsets = pag_res.body_page_offsets;\n const total_doc_pages = pag_res.total_pages;\n\n // Resolve `page` parameter to either \"all\" or a concrete document-page number.\n // Undefined → \"all\" (search across the whole document).\n // \"all\" (case-insensitive) → \"all\".\n // A positive integer N → filter matches to document page N.\n // Anything else → hard error.\n let filter_doc_page: number | null = null; // null means \"all\"\n if (page !== undefined && page !== null) {\n const pageStr = String(page).toLowerCase();\n if (pageStr !== \"all\") {\n const parsed = parseInt(pageStr, 10);\n if (isNaN(parsed) || parsed < 1) {\n throw new Error(\n `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.`,\n );\n }\n if (parsed > total_doc_pages) {\n throw new Error(\n `Document page ${parsed} is out of range — 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.`,\n );\n }\n filter_doc_page = parsed;\n }\n }\n\n // Helper: which document page does an offset live on?\n const pageOfOffset = (offset: number): number => {\n let p = 1;\n for (let j = 0; j < page_offsets.length; j++) {\n if (offset >= page_offsets[j]) p = j + 1;\n else break;\n }\n return p;\n };\n\n // Apply the filter (if any), but keep a record of all pages that had hits\n // so we can show a useful summary even when filtered.\n const pagesWithHits = new Set<number>();\n for (const m of allMatches) {\n pagesWithHits.add(pageOfOffset(m.index!));\n }\n\n const matches =\n filter_doc_page === null\n ? allMatches\n : allMatches.filter((m) => pageOfOffset(m.index!) === filter_doc_page);\n\n // --- Empty result ---\n if (matches.length === 0) {\n let body_msg: string;\n if (filter_doc_page !== null) {\n if (allMatches.length === 0) {\n body_msg = `> **Search Results** — No matches found for query \\`${search_query}\\` in \\`${basename(file_path)}\\`.\\n\\nVerify your search spelling, or try setting \\`search_case_sensitive\\` to false or enabling \\`search_regex\\` if you used pattern wildcards.`;\n } else {\n const hitPages = Array.from(pagesWithHits).sort((a, b) => a - b);\n body_msg = `> **Search Results** — No matches for \\`${search_query}\\` on document page ${filter_doc_page}.\\n\\nThe 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.`;\n }\n } else {\n body_msg = `> **Search Results** — No matches found for query \\`${search_query}\\` in \\`${basename(file_path)}\\`.\\n\\nVerify your search spelling, or try setting \\`search_case_sensitive\\` to false or enabling \\`search_regex\\` if you used pattern wildcards.`;\n }\n if (regexDowngradedNote) body_msg = `${regexDowngradedNote}\\n\\n${body_msg}`;\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${body_msg}`;\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: body_msg,\n title: `Search: ${basename(file_path)}`,\n file_path: resolve(file_path),\n },\n };\n }\n\n // --- Build the response ---\n const ui_parts: string[] = [];\n\n if (filter_doc_page !== null) {\n ui_parts.push(\n `> **Search Results** — 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)}\\`.`,\n );\n const otherPages = Array.from(pagesWithHits)\n .filter((p) => p !== filter_doc_page)\n .sort((a, b) => a - b);\n if (otherPages.length > 0) {\n ui_parts.push(\n `> Additional matches exist on page${otherPages.length !== 1 ? \"s\" : \"\"} ${otherPages.join(\", \")} — omit \\`page\\` or pass \\`page='all'\\` to see them.`,\n );\n }\n } else {\n ui_parts.push(\n `> **Search Results** — Found ${matches.length} match${matches.length !== 1 ? \"es\" : \"\"} for \\`${search_query}\\` in \\`${basename(file_path)}\\`.`,\n );\n if (total_doc_pages > 1) {\n // Build a per-page hit distribution: \"p1: 3, p3: 1, p7: 12\"\n const counts = new Map<number, number>();\n for (const m of allMatches) {\n const p = pageOfOffset(m.index!);\n counts.set(p, (counts.get(p) || 0) + 1);\n }\n const distribution = Array.from(counts.entries())\n .sort((a, b) => a[0] - b[0])\n .map(([p, n]) => `p${p}: ${n}`)\n .join(\", \");\n ui_parts.push(\n `> Distribution across ${total_doc_pages} document pages — ${distribution}. Pass \\`page=N\\` to filter to a specific document page.`,\n );\n }\n }\n\n // Per-match occurrence counts use the FULL match set, not the filtered one —\n // this gives the LLM accurate global counts even when filtering.\n const occurrences_map: Record<string, number> = {};\n for (const m of allMatches) {\n const matched_str = m[0];\n occurrences_map[matched_str] = (occurrences_map[matched_str] || 0) + 1;\n }\n\n const max_matches = 20;\n const is_truncated = matches.length > max_matches;\n const items_to_render = matches.slice(0, max_matches);\n\n if (is_truncated) {\n ui_parts.push(\n `> **Note:** Only the first ${max_matches} matches are shown here to prevent LLM context overflow. ` +\n `Narrow your search query or specify a \\`page\\` filter to see other matches.`\n );\n }\n\n function get_heading(idx: number, txt: string): string {\n const txtBefore = txt.substring(0, idx);\n const lines = txtBefore.split(\"\\n\");\n const path: string[] = [];\n let current_level = 999;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n const m = line.match(/^(#{1,6})\\s+(.*)/);\n if (m) {\n const level = m[1].length;\n if (level < current_level) {\n let cleanHeading = m[2]\n .replace(/\\*\\*|__|[*_]/g, \"\")\n .replace(/\\{#[^}]+\\}/g, \"\")\n .trim();\n if (cleanHeading.length > 80) {\n cleanHeading = cleanHeading.substring(0, 80) + \"...\";\n }\n path.unshift(cleanHeading);\n current_level = level;\n if (level === 1) break;\n }\n }\n }\n return path.join(\" > \");\n }\n\n let i = 1;\n for (const m of items_to_render) {\n const matched_str = m[0];\n const m_start = m.index!;\n const m_end = m_start + matched_str.length;\n const p_num = pageOfOffset(m_start);\n\n const lastNL = m_start <= 0 ? -1 : body.lastIndexOf(\"\\n\", m_start - 1);\n const snippet_start = lastNL === -1 ? 0 : lastNL + 1;\n\n const nextNL = body.indexOf(\"\\n\", m_end);\n const snippet_end = nextNL === -1 ? body.length : nextNL;\n const snippet = emphasizedSnippet(\n body.substring(snippet_start, m_start),\n matched_str,\n body.substring(m_end, snippet_end),\n );\n\n const snippet_lines = snippet\n .split(\"\\n\")\n .filter((line) => line.trim().length > 0)\n .map((line) => `> ${line}`)\n .join(\"\\n\");\n\n ui_parts.push(\"---\");\n ui_parts.push(`### Match ${i} (p${p_num})`);\n\n const h_path = get_heading(m_start, body);\n if (h_path) {\n ui_parts.push(`**Path:** \\`${h_path}\\``);\n }\n\n const count = occurrences_map[matched_str];\n ui_parts.push(snippet_lines);\n ui_parts.push(\n `*Occurrences:* This exact phrasing appears ${count} time${count !== 1 ? \"s\" : \"\"} in the document.`,\n );\n\n i++;\n }\n\n if (regexDowngradedNote) ui_parts.unshift(regexDowngradedNote);\n const ui_markdown = ui_parts.join(\"\\n\\n\");\n const llm_content = `> **File Path:** \\`${resolve(file_path)}\\`\\n\\n${ui_markdown}`;\n\n return {\n content: [{ type: \"text\", text: llm_content }],\n structuredContent: {\n markdown: ui_markdown,\n title: `Search: ${basename(file_path)}`,\n file_path: resolve(file_path),\n },\n };\n}\n","// FILE: node/packages/mcp-server/src/shared.ts\nexport const MARKDOWN_UI_URI = \"ui://adeu/markdown-ui\";\n\n/**\n * Minimal CLI handling BEFORE the stdio server starts: `--help` and\n * `--version` must print and exit like every other executable instead of\n * silently starting the transport (QA 2026-07-19 v8 F-06). Returns the text\n * to print (caller exits without serving), or null to proceed with server\n * startup. Unknown arguments are tolerated — MCP hosts append their own\n * flags. Lives here (not index.ts) so tests can import it without booting\n * the server.\n */\nexport function handleServerCliArgs(\n argv: string[],\n packageVersion: string,\n): string | null {\n if (argv.includes(\"--version\") || argv.includes(\"-v\")) {\n return `adeu-mcp-server ${packageVersion}`;\n }\n if (argv.includes(\"--help\") || argv.includes(\"-h\")) {\n return [\n \"Usage: adeu-mcp-server [options]\",\n \"\",\n \"Adeu MCP server (stdio transport, zero-dependency Node engine).\",\n \"Started by MCP hosts such as Claude Desktop; it reads JSON-RPC on stdin.\",\n \"\",\n \"Options:\",\n \" -h, --help Show this help and exit\",\n \" -v, --version Print the server version and exit\",\n \"\",\n \"Docs: https://github.com/dealfluence/adeu\",\n ].join(\"\\n\");\n }\n return null;\n}\n"],"mappings":"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","names":["basename","resolve","DocumentObject","ui_markdown","llm_content","i","packageVersion","basename","DocumentObject","res","resolve","gitSha"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adeu/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.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.29.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
36
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
37
37
|
"zod": "4.4.3"
|
package/src/index.ts
CHANGED
|
@@ -473,6 +473,13 @@ server.registerTool(
|
|
|
473
473
|
author_name: z
|
|
474
474
|
.string()
|
|
475
475
|
.describe("Name to appear in Track Changes (e.g., 'Reviewer AI')."),
|
|
476
|
+
// Deliberately a plain REQUIRED array of typed items. Wrapping this in
|
|
477
|
+
// z.preprocess (to also accept the whole array as one JSON string) drops
|
|
478
|
+
// it out of the schema's `required` list, and a z.union publishes an
|
|
479
|
+
// anyOf that hides the item schema — both cost more, on every call, than
|
|
480
|
+
// they buy for the rare client that stringifies its payload. That client
|
|
481
|
+
// gets a clear "expected array, received string" it can retry from.
|
|
482
|
+
// Per-item stringification is still tolerated, below and in the engine.
|
|
476
483
|
changes: z
|
|
477
484
|
.array(z.union([z.string(), CHANGE_ITEM_SCHEMA]))
|
|
478
485
|
.describe(
|
|
@@ -516,6 +523,7 @@ server.registerTool(
|
|
|
516
523
|
},
|
|
517
524
|
],
|
|
518
525
|
};
|
|
526
|
+
|
|
519
527
|
if (!changes || changes.length === 0)
|
|
520
528
|
return {
|
|
521
529
|
content: [{ type: "text", text: "Error: No changes provided." }],
|
|
@@ -853,16 +861,26 @@ server.registerTool(
|
|
|
853
861
|
export_pdf: export_pdf as boolean,
|
|
854
862
|
});
|
|
855
863
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
864
|
+
if (result.outBuffer) {
|
|
865
|
+
fs.writeFileSync(outPath, result.outBuffer);
|
|
866
|
+
return {
|
|
867
|
+
content: [
|
|
868
|
+
{
|
|
869
|
+
type: "text",
|
|
870
|
+
text: `Saved to: ${outPath}\n\n${result.reportText}`,
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
};
|
|
874
|
+
} else {
|
|
875
|
+
return {
|
|
876
|
+
content: [
|
|
877
|
+
{
|
|
878
|
+
type: "text",
|
|
879
|
+
text: result.reportText,
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
866
884
|
} catch (e: any) {
|
|
867
885
|
return {
|
|
868
886
|
isError: true,
|
|
@@ -896,7 +914,11 @@ export function formatBatchResult(
|
|
|
896
914
|
? ` (${total_occurrences} occurrences)`
|
|
897
915
|
: "";
|
|
898
916
|
|
|
899
|
-
|
|
917
|
+
const already = stats.actions_already_resolved || 0;
|
|
918
|
+
const already_text = already
|
|
919
|
+
? `, ${already} already resolved (no effect)`
|
|
920
|
+
: "";
|
|
921
|
+
res += `Actions: ${stats.actions_applied} applied, ${stats.actions_skipped} skipped${already_text}.\n`;
|
|
900
922
|
res += `Edits: ${stats.edits_applied} applied${occ_text}, ${stats.edits_skipped} skipped.\n`;
|
|
901
923
|
|
|
902
924
|
if (stats.edits && stats.edits.length > 0) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { spawn, ChildProcess } from "node:child_process";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { DocumentObject, RedlineEngine } from "@adeu/core";
|
|
7
|
+
|
|
8
|
+
describe("QA Regression Test - Finding 1: finalize_document crash on missing sanitize_mode with tracked changes", () => {
|
|
9
|
+
let serverProc: ChildProcess;
|
|
10
|
+
let trackedDocPath: string;
|
|
11
|
+
let outputDocPath: string;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
// 1. Grab the shared golden fixture from the monorepo root
|
|
15
|
+
const fixturePath = resolve(
|
|
16
|
+
__dirname,
|
|
17
|
+
"../../../../shared/fixtures/golden.docx",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
trackedDocPath = join(tmpdir(), `adeu_regression_tracked_${Date.now()}.docx`);
|
|
21
|
+
outputDocPath = join(tmpdir(), `adeu_regression_output_${Date.now()}.docx`);
|
|
22
|
+
|
|
23
|
+
// Load fixture and dirty it to create unresolved tracked changes
|
|
24
|
+
const fixtureBuf = readFileSync(fixturePath);
|
|
25
|
+
const doc = await DocumentObject.load(fixtureBuf);
|
|
26
|
+
const engine = new RedlineEngine(doc, "Reviewer");
|
|
27
|
+
|
|
28
|
+
// Modify a piece of text to generate a tracked change
|
|
29
|
+
engine.process_batch([
|
|
30
|
+
{
|
|
31
|
+
type: "modify",
|
|
32
|
+
target_text: "document",
|
|
33
|
+
new_text: "modified tracked document",
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
writeFileSync(trackedDocPath, await doc.save());
|
|
37
|
+
|
|
38
|
+
// 2. Boot the compiled MCP server
|
|
39
|
+
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
40
|
+
if (!existsSync(serverPath)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"MCP server not built. Run 'npm run build' before running tests.",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
serverProc = spawn("node", [serverPath]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
if (serverProc && !serverProc.killed) serverProc.kill();
|
|
51
|
+
if (existsSync(trackedDocPath)) unlinkSync(trackedDocPath);
|
|
52
|
+
if (existsSync(outputDocPath)) unlinkSync(outputDocPath);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Helper to interact with the stdio JSON-RPC server
|
|
56
|
+
function sendRpc(method: string, params: any, id: number = 1): Promise<any> {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const timeout = setTimeout(() => reject(new Error("RPC Timeout")), 5000);
|
|
59
|
+
|
|
60
|
+
const listener = (data: Buffer) => {
|
|
61
|
+
const lines = data.toString().trim().split("\n");
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
if (!line.startsWith("{")) continue;
|
|
64
|
+
try {
|
|
65
|
+
const res = JSON.parse(line);
|
|
66
|
+
if (res.id === id) {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
serverProc.stdout?.removeListener("data", listener);
|
|
69
|
+
resolve(res);
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Ignore incomplete chunks
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
serverProc.stdout?.on("data", listener);
|
|
78
|
+
serverProc.stdin?.write(
|
|
79
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it("should return a clean block report instead of crashing when sanitize_mode is omitted with tracked changes", async () => {
|
|
85
|
+
const res = await sendRpc(
|
|
86
|
+
"tools/call",
|
|
87
|
+
{
|
|
88
|
+
name: "finalize_document",
|
|
89
|
+
arguments: {
|
|
90
|
+
reasoning: "Finalize document containing unresolved tracked changes without sanitize_mode",
|
|
91
|
+
file_path: trackedDocPath,
|
|
92
|
+
output_path: outputDocPath,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
201,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Assert that the tool does not crash or return a TypeError
|
|
99
|
+
expect(res.error).toBeUndefined();
|
|
100
|
+
expect(res.result).toBeDefined();
|
|
101
|
+
|
|
102
|
+
// It should NOT be an error/crash response.
|
|
103
|
+
// (A blocked finalization is a clean business logic result, not a fatal RPC / NodeJS error)
|
|
104
|
+
expect(res.result.isError).toBeUndefined();
|
|
105
|
+
|
|
106
|
+
const responseText = res.result.content[0].text;
|
|
107
|
+
expect(responseText).not.toContain("TypeError");
|
|
108
|
+
expect(responseText).not.toContain("must be of type string");
|
|
109
|
+
|
|
110
|
+
// It must contain the blocked report indicating unresolved tracked changes
|
|
111
|
+
expect(responseText.toLowerCase()).toContain("blocked");
|
|
112
|
+
expect(responseText).toContain("unresolved tracked changes");
|
|
113
|
+
|
|
114
|
+
// Proves that no file was written
|
|
115
|
+
expect(existsSync(outputDocPath)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { spawn, ChildProcess } from "node:child_process";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
// There is exactly ONE changes parameter. A second `changes_json` spelling
|
|
8
|
+
// forced the model to choose between two ways to say the same thing, and the
|
|
9
|
+
// two engines had drifted into opposite rules for which one wins. `changes`
|
|
10
|
+
// stays a REQUIRED, typed array: per-item stringification is repaired, and a
|
|
11
|
+
// wholly stringified payload is rejected with an error the caller can act on
|
|
12
|
+
// rather than being silently treated as an empty batch.
|
|
13
|
+
describe("QA Regression Test - process_document_batch changes payload shapes", () => {
|
|
14
|
+
let serverProc: ChildProcess;
|
|
15
|
+
let docPath: string;
|
|
16
|
+
let outputDocPath: string;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
const fixturePath = resolve(
|
|
20
|
+
__dirname,
|
|
21
|
+
"../../../../shared/fixtures/golden.docx",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
docPath = join(tmpdir(), `adeu_scratch_doc_${Date.now()}.docx`);
|
|
25
|
+
outputDocPath = join(tmpdir(), `adeu_scratch_output_${Date.now()}.docx`);
|
|
26
|
+
|
|
27
|
+
const fixtureBuf = readFileSync(fixturePath);
|
|
28
|
+
writeFileSync(docPath, fixtureBuf);
|
|
29
|
+
|
|
30
|
+
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
31
|
+
if (!existsSync(serverPath)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"MCP server not built. Run 'npm run build' before running tests.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
serverProc = spawn("node", [serverPath]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
if (serverProc && !serverProc.killed) serverProc.kill();
|
|
42
|
+
if (existsSync(docPath)) unlinkSync(docPath);
|
|
43
|
+
if (existsSync(outputDocPath)) unlinkSync(outputDocPath);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function sendRpc(method: string, params: any, id: number = 1): Promise<any> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const timeout = setTimeout(() => reject(new Error("RPC Timeout")), 5000);
|
|
49
|
+
|
|
50
|
+
const listener = (data: Buffer) => {
|
|
51
|
+
const lines = data.toString().trim().split("\n");
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
if (!line.startsWith("{")) continue;
|
|
54
|
+
try {
|
|
55
|
+
const res = JSON.parse(line);
|
|
56
|
+
if (res.id === id) {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
serverProc.stdout?.removeListener("data", listener);
|
|
59
|
+
resolve(res);
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Ignore incomplete chunks
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
serverProc.stdout?.on("data", listener);
|
|
68
|
+
serverProc.stdin?.write(
|
|
69
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
it("repairs per-item stringified changes (the Gemini double-serialize quirk)", async () => {
|
|
75
|
+
const res = await sendRpc(
|
|
76
|
+
"tools/call",
|
|
77
|
+
{
|
|
78
|
+
name: "process_document_batch",
|
|
79
|
+
arguments: {
|
|
80
|
+
reasoning: "Test per-item stringified changes",
|
|
81
|
+
original_docx_path: docPath,
|
|
82
|
+
output_path: outputDocPath,
|
|
83
|
+
author_name: "QA Tester",
|
|
84
|
+
changes: [
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
type: "modify",
|
|
87
|
+
target_text: "document",
|
|
88
|
+
new_text: "modified tracked document",
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
301,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(res.error).toBeUndefined();
|
|
97
|
+
expect(res.result).toBeDefined();
|
|
98
|
+
expect(res.result.isError).toBeUndefined();
|
|
99
|
+
expect(res.result.content[0].text).toContain("applied");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects a wholly stringified payload loudly instead of as an empty batch", async () => {
|
|
103
|
+
const res = await sendRpc(
|
|
104
|
+
"tools/call",
|
|
105
|
+
{
|
|
106
|
+
name: "process_document_batch",
|
|
107
|
+
arguments: {
|
|
108
|
+
reasoning: "Test wholly stringified payload",
|
|
109
|
+
original_docx_path: docPath,
|
|
110
|
+
output_path: outputDocPath,
|
|
111
|
+
author_name: "QA Tester",
|
|
112
|
+
changes: JSON.stringify([
|
|
113
|
+
{ type: "modify", target_text: "a", new_text: "b" },
|
|
114
|
+
]),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
302,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Must NOT report success over an unchanged document.
|
|
121
|
+
expect(res.result.isError).toBe(true);
|
|
122
|
+
expect(res.result.content[0].text).toContain("expected array");
|
|
123
|
+
expect(res.result.content[0].text).toContain("changes");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("advertises changes as a required, typed array", async () => {
|
|
127
|
+
const res = await sendRpc("tools/list", {}, 303);
|
|
128
|
+
const tool = res.result.tools.find(
|
|
129
|
+
(t: any) => t.name === "process_document_batch",
|
|
130
|
+
);
|
|
131
|
+
expect(tool.inputSchema.required).toContain("changes");
|
|
132
|
+
expect(tool.inputSchema.properties.changes.type).toBe("array");
|
|
133
|
+
expect(tool.inputSchema.properties.changes.items).toBeTruthy();
|
|
134
|
+
expect(Object.keys(tool.inputSchema.properties)).not.toContain(
|
|
135
|
+
"changes_json",
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { spawn, ChildProcess } from "node:child_process";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { DocumentObject } from "@adeu/core";
|
|
7
|
+
|
|
8
|
+
async function createTestDocument(): Promise<DocumentObject> {
|
|
9
|
+
const fixturePath = resolve(__dirname, "../../../../shared/fixtures/initial.docx");
|
|
10
|
+
const buf = readFileSync(fixturePath);
|
|
11
|
+
const doc = await DocumentObject.load(buf);
|
|
12
|
+
const body = doc.element;
|
|
13
|
+
while (body.firstChild) {
|
|
14
|
+
body.removeChild(body.firstChild);
|
|
15
|
+
}
|
|
16
|
+
return doc;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function addParagraph(doc: DocumentObject, text: string): Element {
|
|
20
|
+
const xmlDoc = doc.element.ownerDocument!;
|
|
21
|
+
const p = xmlDoc.createElement('w:p');
|
|
22
|
+
const r = xmlDoc.createElement('w:r');
|
|
23
|
+
const t = xmlDoc.createElement('w:t');
|
|
24
|
+
|
|
25
|
+
t.textContent = text;
|
|
26
|
+
if (text.includes(' ') || text.includes('\n')) {
|
|
27
|
+
t.setAttribute('xml:space', 'preserve');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
r.appendChild(t);
|
|
31
|
+
p.appendChild(r);
|
|
32
|
+
doc.element.appendChild(p);
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("QA Regression Test - read_docx search_query paragraph filtering", () => {
|
|
37
|
+
let serverProc: ChildProcess;
|
|
38
|
+
let testDocPath: string;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
testDocPath = join(tmpdir(), `adeu_regression_unicode_${Date.now()}.docx`);
|
|
42
|
+
|
|
43
|
+
// 1. Build document containing some English text, Chinese text, Accented text, etc.
|
|
44
|
+
const doc = await createTestDocument();
|
|
45
|
+
addParagraph(doc, "Unicode Test Document");
|
|
46
|
+
addParagraph(doc, "This is some English text.");
|
|
47
|
+
addParagraph(doc, "Chinese: 🚀 这是一个测试文档,包含中文字符 and 表情符号。");
|
|
48
|
+
addParagraph(doc, "Accented: Café, naïve, résumé, garçon, déjà vu, Straße.");
|
|
49
|
+
addParagraph(doc, "Emojis & Symbols: 🌟 🦄 💻 ⚡ ️⚽ ️日本語 🌍");
|
|
50
|
+
|
|
51
|
+
writeFileSync(testDocPath, await doc.save());
|
|
52
|
+
|
|
53
|
+
// 2. Boot the compiled MCP server
|
|
54
|
+
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
55
|
+
if (!existsSync(serverPath)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"MCP server not built. Run 'npm run build' before running tests.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
serverProc = spawn("node", [serverPath]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(() => {
|
|
65
|
+
if (serverProc && !serverProc.killed) serverProc.kill();
|
|
66
|
+
if (existsSync(testDocPath)) unlinkSync(testDocPath);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Helper to interact with the stdio JSON-RPC server
|
|
70
|
+
function sendRpc(method: string, params: any, id: number = 1): Promise<any> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const timeout = setTimeout(() => reject(new Error("RPC Timeout")), 5000);
|
|
73
|
+
|
|
74
|
+
const listener = (data: Buffer) => {
|
|
75
|
+
const lines = data.toString().trim().split("\n");
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
if (!line.startsWith("{")) continue;
|
|
78
|
+
try {
|
|
79
|
+
const res = JSON.parse(line);
|
|
80
|
+
if (res.id === id) {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
serverProc.stdout?.removeListener("data", listener);
|
|
83
|
+
resolve(res);
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Ignore incomplete chunks
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
serverProc.stdout?.on("data", listener);
|
|
92
|
+
serverProc.stdin?.write(
|
|
93
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n",
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
it("should filter results to matching paragraphs when search_query is provided", async () => {
|
|
99
|
+
const res = await sendRpc(
|
|
100
|
+
"tools/call",
|
|
101
|
+
{
|
|
102
|
+
name: "read_docx",
|
|
103
|
+
arguments: {
|
|
104
|
+
file_path: testDocPath,
|
|
105
|
+
search_query: "Chinese",
|
|
106
|
+
reasoning: "Filter document to target paragraph.",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
301,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(res.error).toBeUndefined();
|
|
113
|
+
expect(res.result).toBeDefined();
|
|
114
|
+
|
|
115
|
+
// Check that it is NOT a hard error
|
|
116
|
+
expect(res.result.isError).toBeUndefined();
|
|
117
|
+
|
|
118
|
+
const responseText = res.result.content[0].text;
|
|
119
|
+
|
|
120
|
+
// The query MUST be matched and returned (the matched word is highlighted in bold).
|
|
121
|
+
expect(responseText).toContain("**Chinese**: 🚀 这是一个测试文档,包含中文字符 and 表情符号。");
|
|
122
|
+
|
|
123
|
+
// CRITICAL: The search query should filter out non-matching paragraphs to conserve LLM context window tokens.
|
|
124
|
+
// Therefore, other paragraphs in the document should NOT be included in the returned text.
|
|
125
|
+
expect(responseText).not.toContain("This is some English text.");
|
|
126
|
+
expect(responseText).not.toContain("Accented: Café");
|
|
127
|
+
expect(responseText).not.toContain("Emojis & Symbols");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { build_search_response } from "./response-builders.js";
|
|
3
|
+
|
|
4
|
+
describe("Adeu MCP QA Report - Issue 4: Unbounded search results", () => {
|
|
5
|
+
it("TC 4.1: search results are capped to at most 20 matches", () => {
|
|
6
|
+
// Construct a document with 50 matches for "fox"
|
|
7
|
+
const body = Array(50)
|
|
8
|
+
.fill("The quick brown fox jumps over the lazy dog.")
|
|
9
|
+
.join("\n\n");
|
|
10
|
+
|
|
11
|
+
const res = build_search_response(
|
|
12
|
+
body,
|
|
13
|
+
"fox",
|
|
14
|
+
false,
|
|
15
|
+
true,
|
|
16
|
+
undefined,
|
|
17
|
+
"doc.docx",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const md = res.structuredContent!.markdown as string;
|
|
21
|
+
|
|
22
|
+
// We count how many "Match N" headers are in the returned markdown
|
|
23
|
+
const matches = md.match(/### Match \d+/g) || [];
|
|
24
|
+
|
|
25
|
+
// Under current implementation, matches.length is 50.
|
|
26
|
+
// It should be capped at 20.
|
|
27
|
+
expect(matches.length).toBeLessThanOrEqual(20);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/response-builders.ts
CHANGED
|
@@ -61,7 +61,10 @@ function _build_appendix_pointer(has_appendix: boolean): string {
|
|
|
61
61
|
|
|
62
62
|
function _build_page_banner(page: number, total: number): string {
|
|
63
63
|
if (total <= 1) return "";
|
|
64
|
-
|
|
64
|
+
// "synthetic" is load-bearing: Adeu pages are length-based content chunks
|
|
65
|
+
// sized for LLM consumption, and readers must never mistake them for
|
|
66
|
+
// printed Word pages or explicit page breaks (QA 2026-07-19 ADEU-QA-005).
|
|
67
|
+
return `> **Page ${page} of ${total}** (synthetic page — a length-based chunk, not a printed Word page) — call \`read_docx\` with \`mode='outline'\` for a heading map of the full document.\n\n---\n\n`;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function _build_page_footer(
|
|
@@ -451,6 +454,17 @@ export function build_search_response(
|
|
|
451
454
|
occurrences_map[matched_str] = (occurrences_map[matched_str] || 0) + 1;
|
|
452
455
|
}
|
|
453
456
|
|
|
457
|
+
const max_matches = 20;
|
|
458
|
+
const is_truncated = matches.length > max_matches;
|
|
459
|
+
const items_to_render = matches.slice(0, max_matches);
|
|
460
|
+
|
|
461
|
+
if (is_truncated) {
|
|
462
|
+
ui_parts.push(
|
|
463
|
+
`> **Note:** Only the first ${max_matches} matches are shown here to prevent LLM context overflow. ` +
|
|
464
|
+
`Narrow your search query or specify a \`page\` filter to see other matches.`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
454
468
|
function get_heading(idx: number, txt: string): string {
|
|
455
469
|
const txtBefore = txt.substring(0, idx);
|
|
456
470
|
const lines = txtBefore.split("\n");
|
|
@@ -480,14 +494,17 @@ export function build_search_response(
|
|
|
480
494
|
}
|
|
481
495
|
|
|
482
496
|
let i = 1;
|
|
483
|
-
for (const m of
|
|
497
|
+
for (const m of items_to_render) {
|
|
484
498
|
const matched_str = m[0];
|
|
485
499
|
const m_start = m.index!;
|
|
486
500
|
const m_end = m_start + matched_str.length;
|
|
487
501
|
const p_num = pageOfOffset(m_start);
|
|
488
502
|
|
|
489
|
-
const
|
|
490
|
-
const
|
|
503
|
+
const lastNL = m_start <= 0 ? -1 : body.lastIndexOf("\n", m_start - 1);
|
|
504
|
+
const snippet_start = lastNL === -1 ? 0 : lastNL + 1;
|
|
505
|
+
|
|
506
|
+
const nextNL = body.indexOf("\n", m_end);
|
|
507
|
+
const snippet_end = nextNL === -1 ? body.length : nextNL;
|
|
491
508
|
const snippet = emphasizedSnippet(
|
|
492
509
|
body.substring(snippet_start, m_start),
|
|
493
510
|
matched_str,
|
|
@@ -3,48 +3,52 @@ from docx import Document
|
|
|
3
3
|
from docx.oxml.ns import qn
|
|
4
4
|
from docx.oxml import OxmlElement
|
|
5
5
|
|
|
6
|
-
os.makedirs(
|
|
6
|
+
os.makedirs("node/packages/mcp-server/tests/fixtures", exist_ok=True)
|
|
7
7
|
|
|
8
8
|
doc = Document()
|
|
9
|
-
doc.add_paragraph("Section One").style = doc.styles[
|
|
9
|
+
doc.add_paragraph("Section One").style = doc.styles["Heading 1"]
|
|
10
10
|
p = doc.add_paragraph()
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
def run(text):
|
|
13
|
-
r = OxmlElement(
|
|
14
|
-
t = OxmlElement(
|
|
14
|
+
r = OxmlElement("w:r")
|
|
15
|
+
t = OxmlElement("w:t")
|
|
15
16
|
t.text = text
|
|
16
|
-
t.set(qn(
|
|
17
|
+
t.set(qn("xml:space"), "preserve")
|
|
17
18
|
r.append(t)
|
|
18
19
|
return r
|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
p._p.append(run("Foo bar "))
|
|
21
23
|
|
|
22
|
-
d = OxmlElement(
|
|
23
|
-
d.set(qn(
|
|
24
|
-
d.set(qn(
|
|
25
|
-
d.set(qn(
|
|
24
|
+
d = OxmlElement("w:del")
|
|
25
|
+
d.set(qn("w:id"), "10")
|
|
26
|
+
d.set(qn("w:author"), "Test Negotiator")
|
|
27
|
+
d.set(qn("w:date"), "2026-01-22T12:06:55Z")
|
|
26
28
|
|
|
27
|
-
rd = OxmlElement(
|
|
28
|
-
dt = OxmlElement(
|
|
29
|
+
rd = OxmlElement("w:r")
|
|
30
|
+
dt = OxmlElement("w:delText")
|
|
29
31
|
dt.text = "old phrase here."
|
|
30
|
-
dt.set(qn(
|
|
32
|
+
dt.set(qn("xml:space"), "preserve")
|
|
31
33
|
rd.append(dt)
|
|
32
34
|
d.append(rd)
|
|
33
35
|
p._p.append(d)
|
|
34
36
|
|
|
35
|
-
ins = OxmlElement(
|
|
36
|
-
ins.set(qn(
|
|
37
|
-
ins.set(qn(
|
|
38
|
-
ins.set(qn(
|
|
37
|
+
ins = OxmlElement("w:ins")
|
|
38
|
+
ins.set(qn("w:id"), "11")
|
|
39
|
+
ins.set(qn("w:author"), "Test Negotiator")
|
|
40
|
+
ins.set(qn("w:date"), "2026-01-22T12:06:55Z")
|
|
39
41
|
ins.append(run("new phrase here."))
|
|
40
42
|
p._p.append(ins)
|
|
41
43
|
|
|
42
|
-
doc.save(
|
|
43
|
-
print(
|
|
44
|
+
doc.save("node/packages/mcp-server/tests/fixtures/gap2_minimal_repro.docx")
|
|
45
|
+
print(
|
|
46
|
+
"Successfully generated node/packages/mcp-server/tests/fixtures/gap2_minimal_repro.docx"
|
|
47
|
+
)
|
|
44
48
|
|
|
45
49
|
# Generate GAP 1 deleted row fixture
|
|
46
50
|
doc1 = Document()
|
|
47
|
-
doc1.add_paragraph("Active Heading").style = doc1.styles[
|
|
51
|
+
doc1.add_paragraph("Active Heading").style = doc1.styles["Heading 1"]
|
|
48
52
|
|
|
49
53
|
# Add a table
|
|
50
54
|
table = doc1.add_table(rows=1, cols=1)
|
|
@@ -53,17 +57,19 @@ cell = row.cells[0]
|
|
|
53
57
|
|
|
54
58
|
# Add a paragraph inside the cell with Heading 1 style
|
|
55
59
|
p_cell = cell.paragraphs[0]
|
|
56
|
-
p_cell.style = doc1.styles[
|
|
60
|
+
p_cell.style = doc1.styles["Heading 1"]
|
|
57
61
|
# Clear default text and append the text
|
|
58
62
|
p_cell.text = "Deleted Heading"
|
|
59
63
|
|
|
60
64
|
# Now let's mark the row as deleted (w:del inside w:trPr)
|
|
61
65
|
trPr = row._tr.get_or_add_trPr()
|
|
62
|
-
del_node = OxmlElement(
|
|
63
|
-
del_node.set(qn(
|
|
64
|
-
del_node.set(qn(
|
|
65
|
-
del_node.set(qn(
|
|
66
|
+
del_node = OxmlElement("w:del")
|
|
67
|
+
del_node.set(qn("w:id"), "100")
|
|
68
|
+
del_node.set(qn("w:author"), "Test Negotiator")
|
|
69
|
+
del_node.set(qn("w:date"), "2026-01-22T12:06:55Z")
|
|
66
70
|
trPr.append(del_node)
|
|
67
71
|
|
|
68
|
-
doc1.save(
|
|
69
|
-
print(
|
|
72
|
+
doc1.save("node/packages/mcp-server/tests/fixtures/gap1_deleted_row_repro.docx")
|
|
73
|
+
print(
|
|
74
|
+
"Successfully generated node/packages/mcp-server/tests/fixtures/gap1_deleted_row_repro.docx"
|
|
75
|
+
)
|