@adeu/mcp-server 1.18.0 → 1.18.2

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.
@@ -6,7 +6,10 @@ import { existsSync, readFileSync } from "node:fs";
6
6
 
7
7
  describe("Parity Live Server Integration Verification", () => {
8
8
  let serverProc: ChildProcess;
9
- const fixturePath = resolve(__dirname, "../tests/fixtures/gap2_minimal_repro.docx");
9
+ const fixturePath = resolve(
10
+ __dirname,
11
+ "../tests/fixtures/gap2_minimal_repro.docx",
12
+ );
10
13
 
11
14
  beforeAll(async () => {
12
15
  const serverPath = resolve(__dirname, "../dist/index.js");
@@ -76,86 +79,117 @@ describe("Parity Live Server Integration Verification", () => {
76
79
  });
77
80
 
78
81
  it("exposes the build stamp via serverInfo.version in initialize", async () => {
79
- const initRes = await sendRpc("initialize", {
80
- protocolVersion: "2024-11-05",
81
- capabilities: {},
82
- clientInfo: { name: "test-client", version: "1.0.0" }
83
- }, 202);
82
+ const initRes = await sendRpc(
83
+ "initialize",
84
+ {
85
+ protocolVersion: "2024-11-05",
86
+ capabilities: {},
87
+ clientInfo: { name: "test-client", version: "1.0.0" },
88
+ },
89
+ 202,
90
+ );
84
91
  expect(initRes.result).toBeDefined();
85
92
  expect(initRes.result.serverInfo).toBeDefined();
86
93
  expect(initRes.result.serverInfo.version).not.toBe("1.0.0");
87
94
  expect(initRes.result.serverInfo.version).not.toContain("unknown");
88
95
  });
89
96
 
90
-
91
97
  it("read_docx appends [Debug] build stamp footer to mode=outline and mode=full", async () => {
92
- const res = await sendRpc("tools/call", {
93
- name: "read_docx",
94
- arguments: {
95
- file_path: fixturePath,
96
- mode: "outline",
98
+ const res = await sendRpc(
99
+ "tools/call",
100
+ {
101
+ name: "read_docx",
102
+ arguments: {
103
+ reasoning: "test",
104
+ file_path: fixturePath,
105
+ mode: "outline",
106
+ },
97
107
  },
98
- }, 202);
108
+ 202,
109
+ );
99
110
 
100
111
  expect(res.result).toBeDefined();
101
112
  expect(res.result.content[0].text).not.toContain("[Debug] build=");
102
113
  });
103
114
 
104
115
  it("GAP 2 (Live): process_document_batch modify straddling deleted text returns actionable deletion error", async () => {
105
- const outPath = join(resolve(__dirname, "../../../../tmp"), `live_gap2_out_${Date.now()}.docx`);
106
- const res = await sendRpc("tools/call", {
107
- name: "process_document_batch",
108
- arguments: {
109
- original_docx_path: fixturePath,
110
- author_name: "Ed",
111
- changes: [
112
- {
113
- type: "modify",
114
- target_text: "Foo bar old phrase here.",
115
- new_text: "X",
116
- comment: "c",
117
- }
118
- ],
119
- output_path: outPath,
120
- dry_run: true,
116
+ const outPath = join(
117
+ resolve(__dirname, "../../../../tmp"),
118
+ `live_gap2_out_${Date.now()}.docx`,
119
+ );
120
+ const res = await sendRpc(
121
+ "tools/call",
122
+ {
123
+ name: "process_document_batch",
124
+ arguments: {
125
+ reasoning: "test",
126
+ original_docx_path: fixturePath,
127
+ author_name: "Ed",
128
+ changes: [
129
+ {
130
+ type: "modify",
131
+ target_text: "Foo bar old phrase here.",
132
+ new_text: "X",
133
+ comment: "c",
134
+ },
135
+ ],
136
+ output_path: outPath,
137
+ dry_run: true,
138
+ },
121
139
  },
122
- }, 203);
140
+ 203,
141
+ );
123
142
 
124
143
  expect(res.result).toBeDefined();
125
144
  const text = res.result.content[0].text;
126
- expect(text).toContain("Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.");
127
- expect(text).toContain("Reject/accept that change first or target the active replacement text instead.");
145
+ expect(text).toContain(
146
+ "Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.",
147
+ );
148
+ expect(text).toContain(
149
+ "Reject/accept that change first or target the active replacement text instead.",
150
+ );
128
151
  expect(text).not.toContain("Target text not found in document.");
129
152
 
130
153
  // Run Python engine to assert cross-engine exact error parity
131
154
  const { execSync } = await import("node:child_process");
132
155
  const projectRoot = resolve(__dirname, "../../../..");
133
- const pythonCli = process.platform === "win32"
134
- ? join(projectRoot, "python/.venv/Scripts/adeu.exe")
135
- : join(projectRoot, "python/.venv/bin/adeu");
136
-
156
+ const pythonCli =
157
+ process.platform === "win32"
158
+ ? join(projectRoot, "python/.venv/Scripts/adeu.exe")
159
+ : join(projectRoot, "python/.venv/bin/adeu");
160
+
137
161
  // Create temporary changes JSON for Python CLI
138
162
  const fs = await import("node:fs");
139
163
  const tempJsonPath = join(projectRoot, "tmp", `changes_${Date.now()}.json`);
140
164
  fs.mkdirSync(join(projectRoot, "tmp"), { recursive: true });
141
- fs.writeFileSync(tempJsonPath, JSON.stringify([
142
- {
143
- type: "modify",
144
- target_text: "Foo bar old phrase here.",
145
- new_text: "X",
146
- comment: "c",
147
- }
148
- ]));
165
+ fs.writeFileSync(
166
+ tempJsonPath,
167
+ JSON.stringify([
168
+ {
169
+ type: "modify",
170
+ target_text: "Foo bar old phrase here.",
171
+ new_text: "X",
172
+ comment: "c",
173
+ },
174
+ ]),
175
+ );
149
176
 
150
177
  try {
151
- const pythonOut = execSync(`"${pythonCli}" apply "${fixturePath}" "${tempJsonPath}" --dry-run --author Ed`, {
152
- encoding: "utf-8",
153
- });
178
+ const pythonOut = execSync(
179
+ `"${pythonCli}" apply "${fixturePath}" "${tempJsonPath}" --dry-run --author Ed`,
180
+ {
181
+ encoding: "utf-8",
182
+ },
183
+ );
154
184
  // Should not succeed normally since dry run exits with code 1 on errors, so we catch in the try block
155
185
  } catch (err: any) {
156
186
  const pyErrorOutput = err.stdout || err.stderr || "";
157
- expect(pyErrorOutput).toContain("Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.");
158
- expect(pyErrorOutput).toContain("Reject/accept that change first or target the active replacement text instead.");
187
+ expect(pyErrorOutput).toContain(
188
+ "Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.",
189
+ );
190
+ expect(pyErrorOutput).toContain(
191
+ "Reject/accept that change first or target the active replacement text instead.",
192
+ );
159
193
  } finally {
160
194
  if (fs.existsSync(tempJsonPath)) {
161
195
  fs.unlinkSync(tempJsonPath);
@@ -164,34 +198,50 @@ describe("Parity Live Server Integration Verification", () => {
164
198
  });
165
199
 
166
200
  it("GAP 1 (Live): read_docx mode=outline heading count and content parity with Python", async () => {
167
- const gap1FixturePath = resolve(__dirname, "../tests/fixtures/gap1_deleted_row_repro.docx");
201
+ const gap1FixturePath = resolve(
202
+ __dirname,
203
+ "../tests/fixtures/gap1_deleted_row_repro.docx",
204
+ );
168
205
  const { execSync } = await import("node:child_process");
169
206
  const projectRoot = resolve(__dirname, "../../../..");
170
- const pythonCli = process.platform === "win32"
171
- ? join(projectRoot, "python/.venv/Scripts/adeu.exe")
172
- : join(projectRoot, "python/.venv/bin/adeu");
207
+ const pythonCli =
208
+ process.platform === "win32"
209
+ ? join(projectRoot, "python/.venv/Scripts/adeu.exe")
210
+ : join(projectRoot, "python/.venv/bin/adeu");
173
211
 
174
212
  // 1. clean_view = true
175
- const nodeResClean = await sendRpc("tools/call", {
176
- name: "read_docx",
177
- arguments: {
178
- file_path: gap1FixturePath,
179
- mode: "outline",
180
- clean_view: true,
213
+ const nodeResClean = await sendRpc(
214
+ "tools/call",
215
+ {
216
+ name: "read_docx",
217
+ arguments: {
218
+ reasoning: "test",
219
+ file_path: gap1FixturePath,
220
+ mode: "outline",
221
+ clean_view: true,
222
+ },
181
223
  },
182
- }, 204);
224
+ 204,
225
+ );
183
226
 
184
227
  expect(nodeResClean.result).toBeDefined();
185
228
  const nodeTextClean = nodeResClean.result.content[0].text;
186
-
229
+
187
230
  // Get Python output for clean_view = true
188
- const pythonOutClean = execSync(`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`, {
189
- encoding: "utf-8",
190
- });
231
+ const pythonOutClean = execSync(
232
+ `"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`,
233
+ {
234
+ encoding: "utf-8",
235
+ },
236
+ );
191
237
 
192
238
  // Extract raw markdown headings (lines starting with #) to assert perfect parity
193
- const getHeadings = (text: string) => text.split("\n").filter(line => line.startsWith("#")).map(l => l.trim());
194
-
239
+ const getHeadings = (text: string) =>
240
+ text
241
+ .split("\n")
242
+ .filter((line) => line.startsWith("#"))
243
+ .map((l) => l.trim());
244
+
195
245
  const nodeHeadingsClean = getHeadings(nodeTextClean);
196
246
  const pythonHeadingsClean = getHeadings(pythonOutClean);
197
247
 
@@ -200,56 +250,86 @@ describe("Parity Live Server Integration Verification", () => {
200
250
  expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
201
251
 
202
252
  // 2. clean_view = false
203
- const nodeResDirty = await sendRpc("tools/call", {
204
- name: "read_docx",
205
- arguments: {
206
- file_path: gap1FixturePath,
207
- mode: "outline",
208
- clean_view: false,
253
+ const nodeResDirty = await sendRpc(
254
+ "tools/call",
255
+ {
256
+ name: "read_docx",
257
+ arguments: {
258
+ reasoning: "test",
259
+ file_path: gap1FixturePath,
260
+ mode: "outline",
261
+ clean_view: false,
262
+ },
209
263
  },
210
- }, 205);
264
+ 205,
265
+ );
211
266
 
212
267
  expect(nodeResDirty.result).toBeDefined();
213
268
  const nodeTextDirty = nodeResDirty.result.content[0].text;
214
269
 
215
- const pythonOutDirty = execSync(`"${pythonCli}" extract "${gap1FixturePath}" --mode outline`, {
216
- encoding: "utf-8",
217
- });
270
+ const pythonOutDirty = execSync(
271
+ `"${pythonCli}" extract "${gap1FixturePath}" --mode outline`,
272
+ {
273
+ encoding: "utf-8",
274
+ },
275
+ );
218
276
 
219
277
  const nodeHeadingsDirty = getHeadings(nodeTextDirty);
220
278
  const pythonHeadingsDirty = getHeadings(pythonOutDirty);
221
279
 
222
- expect(nodeHeadingsDirty).toEqual(["# Active Heading (p1)", "# Deleted Heading (p1)"]);
223
- expect(pythonHeadingsDirty).toEqual(["# Active Heading (p1)", "# Deleted Heading (p1)"]);
280
+ expect(nodeHeadingsDirty).toEqual([
281
+ "# Active Heading (p1)",
282
+ "# Deleted Heading (p1)",
283
+ ]);
284
+ expect(pythonHeadingsDirty).toEqual([
285
+ "# Active Heading (p1)",
286
+ "# Deleted Heading (p1)",
287
+ ]);
224
288
  expect(nodeHeadingsDirty).toEqual(pythonHeadingsDirty);
225
289
  });
226
290
 
227
291
  it("GAP 1 - Style Def (Live): style-definition outlineLvl on non-heading style classification parity", async () => {
228
- const gap1FixturePath = resolve(__dirname, "../tests/fixtures/gap1_minimal_repro.docx");
292
+ const gap1FixturePath = resolve(
293
+ __dirname,
294
+ "../tests/fixtures/gap1_minimal_repro.docx",
295
+ );
229
296
  const { execSync } = await import("node:child_process");
230
297
  const projectRoot = resolve(__dirname, "../../../..");
231
- const pythonCli = process.platform === "win32"
232
- ? join(projectRoot, "python/.venv/Scripts/adeu.exe")
233
- : join(projectRoot, "python/.venv/bin/adeu");
298
+ const pythonCli =
299
+ process.platform === "win32"
300
+ ? join(projectRoot, "python/.venv/Scripts/adeu.exe")
301
+ : join(projectRoot, "python/.venv/bin/adeu");
234
302
 
235
303
  // 1. clean_view = true (both engines must return exactly 2 headings, excluding Subtitle)
236
- const nodeResClean = await sendRpc("tools/call", {
237
- name: "read_docx",
238
- arguments: {
239
- file_path: gap1FixturePath,
240
- mode: "outline",
241
- clean_view: true,
304
+ const nodeResClean = await sendRpc(
305
+ "tools/call",
306
+ {
307
+ name: "read_docx",
308
+ arguments: {
309
+ reasoning: "test",
310
+ file_path: gap1FixturePath,
311
+ mode: "outline",
312
+ clean_view: true,
313
+ },
242
314
  },
243
- }, 206);
315
+ 206,
316
+ );
244
317
 
245
318
  expect(nodeResClean.result).toBeDefined();
246
319
  const nodeTextClean = nodeResClean.result.content[0].text;
247
320
 
248
- const pythonOutClean = execSync(`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`, {
249
- encoding: "utf-8",
250
- });
321
+ const pythonOutClean = execSync(
322
+ `"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`,
323
+ {
324
+ encoding: "utf-8",
325
+ },
326
+ );
251
327
 
252
- const getHeadings = (text: string) => text.split("\n").filter(line => line.startsWith("#")).map(l => l.trim());
328
+ const getHeadings = (text: string) =>
329
+ text
330
+ .split("\n")
331
+ .filter((line) => line.startsWith("#"))
332
+ .map((l) => l.trim());
253
333
 
254
334
  const nodeHeadingsClean = getHeadings(nodeTextClean);
255
335
  const pythonHeadingsClean = getHeadings(pythonOutClean);
@@ -258,8 +338,14 @@ describe("Parity Live Server Integration Verification", () => {
258
338
  expect(nodeHeadingsClean.length).toBe(2);
259
339
  expect(pythonHeadingsClean.length).toBe(2);
260
340
 
261
- expect(nodeHeadingsClean).toEqual(["# Real Heading One (p1)", "# Real Heading Two (p1)"]);
262
- expect(pythonHeadingsClean).toEqual(["# Real Heading One (p1)", "# Real Heading Two (p1)"]);
341
+ expect(nodeHeadingsClean).toEqual([
342
+ "# Real Heading One (p1)",
343
+ "# Real Heading Two (p1)",
344
+ ]);
345
+ expect(pythonHeadingsClean).toEqual([
346
+ "# Real Heading One (p1)",
347
+ "# Real Heading Two (p1)",
348
+ ]);
263
349
  expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
264
350
  });
265
351
  });
@@ -57,14 +57,17 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
57
57
 
58
58
  const serverPath = resolve(__dirname, "../dist/index.js");
59
59
  if (!existsSync(serverPath)) {
60
- throw new Error("MCP server not built. Run 'npm run build' before tests.");
60
+ throw new Error(
61
+ "MCP server not built. Run 'npm run build' before tests.",
62
+ );
61
63
  }
62
64
  serverProc = spawn("node", [serverPath]);
63
65
  });
64
66
 
65
67
  afterAll(() => {
66
68
  if (serverProc && !serverProc.killed) serverProc.kill();
67
- if (workDir && existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
69
+ if (workDir && existsSync(workDir))
70
+ rmSync(workDir, { recursive: true, force: true });
68
71
  });
69
72
 
70
73
  function sendRpc(method: string, params: any, id: number): Promise<any> {
@@ -105,9 +108,12 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
105
108
  const r1 = await callTool(
106
109
  "process_document_batch",
107
110
  {
111
+ reasoning: "test",
108
112
  original_docx_path: contractPath,
109
113
  author_name: "Agent",
110
- changes: [{ type: "modify", target_text: "Provider", new_text: "Supplier" }],
114
+ changes: [
115
+ { type: "modify", target_text: "Provider", new_text: "Supplier" },
116
+ ],
111
117
  },
112
118
  201,
113
119
  );
@@ -120,7 +126,10 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
120
126
  // NOT back onto contract.docx / contract_processed.docx.
121
127
  const r2 = await callTool(
122
128
  "accept_all_changes",
123
- { docx_path: join(workDir, "contract_processed.docx") },
129
+ {
130
+ reasoning: "test",
131
+ docx_path: join(workDir, "contract_processed.docx"),
132
+ },
124
133
  202,
125
134
  );
126
135
  const msg2 = r2.result.content[0].text as string;
@@ -145,15 +154,20 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
145
154
  const r = await callTool(
146
155
  "process_document_batch",
147
156
  {
157
+ reasoning: "test",
148
158
  original_docx_path: join(workDir, "contract_processed.docx"),
149
159
  author_name: "Agent",
150
- changes: [{ type: "modify", target_text: "goods", new_text: "products" }],
160
+ changes: [
161
+ { type: "modify", target_text: "goods", new_text: "products" },
162
+ ],
151
163
  },
152
164
  203,
153
165
  );
154
166
  const msg = r.result.content[0].text as string;
155
167
  expect(savedFile(msg)).toBe("contract_processed.docx");
156
- expect(existsSync(join(workDir, "contract_processed_processed.docx"))).toBe(false);
168
+ expect(existsSync(join(workDir, "contract_processed_processed.docx"))).toBe(
169
+ false,
170
+ );
157
171
  });
158
172
 
159
173
  it("Issue 1 (end-to-end via shipped server): accept + modify the same foreign insertion in one batch succeeds", async () => {
@@ -162,7 +176,9 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
162
176
  const foreign = join(workDir, "lease.docx");
163
177
  {
164
178
  const doc = await DocumentObject.load(
165
- readFileSync(resolve(__dirname, "../../../../shared/fixtures/initial.docx")),
179
+ readFileSync(
180
+ resolve(__dirname, "../../../../shared/fixtures/initial.docx"),
181
+ ),
166
182
  );
167
183
  const body = doc.element;
168
184
  while (body.firstChild) body.removeChild(body.firstChild);
@@ -191,6 +207,7 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
191
207
  const res = await callTool(
192
208
  "process_document_batch",
193
209
  {
210
+ reasoning: "test",
194
211
  original_docx_path: foreign,
195
212
  author_name: "Acme's Counsel",
196
213
  output_path: join(workDir, "lease_out.docx"),
@@ -220,13 +220,27 @@ export function build_search_response(
220
220
  const [body] = split_structural_appendix(text);
221
221
  const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
222
222
  const flags = search_case_sensitive ? "g" : "gi";
223
- const patternStr = search_regex ? search_query : escapeRegExp(search_query);
224
223
 
224
+ // When the caller asked for a regex but supplied something the engine can't
225
+ // compile (e.g. an unterminated character class `\[`, or an inline-flag group
226
+ // `(?i)...` that JS RegExp rejects), do NOT hard-error and burn the turn.
227
+ // Downgrade to a literal search of the raw string and tell the model, so it
228
+ // can either accept the literal hits or fix its pattern — instead of retrying
229
+ // the same broken regex.
230
+ let regexDowngradedNote = "";
225
231
  let regex: RegExp;
226
- try {
227
- regex = new RegExp(patternStr, flags);
228
- } catch (e: any) {
229
- throw new Error(`Invalid regex pattern: ${e.message}`);
232
+ if (search_regex) {
233
+ try {
234
+ regex = new RegExp(search_query, flags);
235
+ } catch (e: any) {
236
+ regexDowngradedNote =
237
+ `> **Note:** \`${search_query}\` is not a valid regular expression ` +
238
+ `(${e.message}), so it was searched as literal text instead. ` +
239
+ `If you meant a regex, fix the pattern; if you meant literal text, set \`search_regex\` to false.`;
240
+ regex = new RegExp(escapeRegExp(search_query), flags);
241
+ }
242
+ } else {
243
+ regex = new RegExp(escapeRegExp(search_query), flags);
230
244
  }
231
245
 
232
246
  const allMatches = Array.from(body.matchAll(regex));
@@ -295,6 +309,7 @@ export function build_search_response(
295
309
  } else {
296
310
  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.`;
297
311
  }
312
+ if (regexDowngradedNote) body_msg = `${regexDowngradedNote}\n\n${body_msg}`;
298
313
  const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${body_msg}`;
299
314
  return {
300
315
  content: [{ type: "text", text: llm_content }],
@@ -415,6 +430,7 @@ export function build_search_response(
415
430
  i++;
416
431
  }
417
432
 
433
+ if (regexDowngradedNote) ui_parts.unshift(regexDowngradedNote);
418
434
  const ui_markdown = ui_parts.join("\n\n");
419
435
  const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
420
436