@adeu/mcp-server 1.6.7 → 1.6.9
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 +302 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +378 -144
- package/src/mcp.bugs.test.ts +162 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// FILE: node/packages/mcp-server/src/mcp.bugs.test.ts
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
3
|
+
import { spawn, ChildProcess } from "node:child_process";
|
|
4
|
+
import { resolve, join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { DocumentObject, RedlineEngine } from "@adeu/core";
|
|
8
|
+
|
|
9
|
+
describe("Resolved Bugs MCP Server Verification", () => {
|
|
10
|
+
let serverProc: ChildProcess;
|
|
11
|
+
let cleanDocPath: string;
|
|
12
|
+
let dirtyDocPath: string;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
// 1. Grab the shared golden fixture from the monorepo root
|
|
16
|
+
const fixturePath = resolve(
|
|
17
|
+
__dirname,
|
|
18
|
+
"../../../../shared/fixtures/golden.docx",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
cleanDocPath = join(tmpdir(), `adeu_clean_${Date.now()}.docx`);
|
|
22
|
+
dirtyDocPath = join(tmpdir(), `adeu_dirty_${Date.now()}.docx`);
|
|
23
|
+
|
|
24
|
+
// Save a clean copy
|
|
25
|
+
const fixtureBuf = readFileSync(fixturePath);
|
|
26
|
+
writeFileSync(cleanDocPath, fixtureBuf);
|
|
27
|
+
|
|
28
|
+
// Load it via the public API, dirty it, and save a dirty copy
|
|
29
|
+
const doc = await DocumentObject.load(fixtureBuf);
|
|
30
|
+
const engine = new RedlineEngine(doc, "Reviewer");
|
|
31
|
+
|
|
32
|
+
// "document" is original base text, so we won't trigger the cross-author nested redline constraint
|
|
33
|
+
engine.process_batch([
|
|
34
|
+
{
|
|
35
|
+
type: "modify",
|
|
36
|
+
target_text: "document",
|
|
37
|
+
new_text: "dirty modified document",
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
writeFileSync(dirtyDocPath, await doc.save());
|
|
41
|
+
|
|
42
|
+
// 2. Boot the compiled MCP server
|
|
43
|
+
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
44
|
+
if (!existsSync(serverPath)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"MCP server not built. Run 'npm run build' before tests.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
serverProc = spawn("node", [serverPath]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
if (serverProc && !serverProc.killed) serverProc.kill();
|
|
55
|
+
if (existsSync(cleanDocPath)) unlinkSync(cleanDocPath);
|
|
56
|
+
if (existsSync(dirtyDocPath)) unlinkSync(dirtyDocPath);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Helper to interact with the stdio JSON-RPC server
|
|
60
|
+
function sendRpc(method: string, params: any, id: number = 1): Promise<any> {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timeout = setTimeout(() => reject(new Error("RPC Timeout")), 5000);
|
|
63
|
+
|
|
64
|
+
const listener = (data: Buffer) => {
|
|
65
|
+
const lines = data.toString().trim().split("\n");
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (!line.startsWith("{")) continue;
|
|
68
|
+
try {
|
|
69
|
+
const res = JSON.parse(line);
|
|
70
|
+
if (res.id === id) {
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
serverProc.stdout?.removeListener("data", listener);
|
|
73
|
+
resolve(res);
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Ignore incomplete chunks
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
serverProc.stdout?.on("data", listener);
|
|
82
|
+
serverProc.stdin?.write(
|
|
83
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
it("BUG-5: Rejects empty changes array early without writing files", async () => {
|
|
89
|
+
const outPath = join(tmpdir(), `adeu_out_${Date.now()}.docx`);
|
|
90
|
+
if (existsSync(outPath)) unlinkSync(outPath);
|
|
91
|
+
|
|
92
|
+
const res = await sendRpc(
|
|
93
|
+
"tools/call",
|
|
94
|
+
{
|
|
95
|
+
name: "process_document_batch",
|
|
96
|
+
arguments: {
|
|
97
|
+
original_docx_path: cleanDocPath,
|
|
98
|
+
author_name: "Agent",
|
|
99
|
+
changes: [],
|
|
100
|
+
output_path: outPath,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
101,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(res.result.content[0].text).toBe("Error: No changes provided.");
|
|
107
|
+
expect(existsSync(outPath)).toBe(false); // Proves no-op
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("BUG-9: diff_docx_files tool respects compare_clean parameter", async () => {
|
|
111
|
+
// 1. compare_clean = true (Default) -> Should output clean text comparison
|
|
112
|
+
const resClean = await sendRpc(
|
|
113
|
+
"tools/call",
|
|
114
|
+
{
|
|
115
|
+
name: "diff_docx_files",
|
|
116
|
+
arguments: {
|
|
117
|
+
original_path: cleanDocPath,
|
|
118
|
+
modified_path: dirtyDocPath,
|
|
119
|
+
compare_clean: true,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
102,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const cleanText = resClean.result.content[0].text;
|
|
126
|
+
expect(cleanText).toContain("dirty modified");
|
|
127
|
+
expect(cleanText).not.toContain("{++"); // No CriticMarkup
|
|
128
|
+
|
|
129
|
+
// 2. compare_clean = false -> Should output raw CriticMarkup comparison
|
|
130
|
+
const resRaw = await sendRpc(
|
|
131
|
+
"tools/call",
|
|
132
|
+
{
|
|
133
|
+
name: "diff_docx_files",
|
|
134
|
+
arguments: {
|
|
135
|
+
original_path: cleanDocPath,
|
|
136
|
+
modified_path: dirtyDocPath,
|
|
137
|
+
compare_clean: false,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
103,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const rawText = resRaw.result.content[0].text;
|
|
144
|
+
expect(rawText).toContain("{++dirty modified ++}");
|
|
145
|
+
});
|
|
146
|
+
it("BUG-10: Traps ENOENT and returns clean File Not Found errors", async () => {
|
|
147
|
+
const res = await sendRpc(
|
|
148
|
+
"tools/call",
|
|
149
|
+
{
|
|
150
|
+
name: "read_docx",
|
|
151
|
+
arguments: { file_path: join(tmpdir(), "DEF_DOES_NOT_EXIST.docx") },
|
|
152
|
+
},
|
|
153
|
+
104,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(res.result.isError).toBe(true);
|
|
157
|
+
expect(res.result.content[0].text).toContain(
|
|
158
|
+
"Error executing tool read_docx: File not found:",
|
|
159
|
+
);
|
|
160
|
+
expect(res.result.content[0].text).not.toContain("ENOENT"); // Raw node error must not leak
|
|
161
|
+
});
|
|
162
|
+
});
|