@adeu/mcp-server 1.10.0 → 1.11.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.
- package/README.md +15 -1
- package/dist/index.js +61 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/scripts/verify-bundle.js +50 -0
- package/src/index.ts +79 -17
- package/src/mcp.bugs.test.ts +2 -0
- package/src/parity_live.test.ts +259 -0
- package/src/response-builders.ts +2 -0
- package/src/tools/auth.ts +8 -1
- package/tests/fixtures/gap1_deleted_row_repro.docx +0 -0
- package/tests/fixtures/gap1_minimal_repro.docx +0 -0
- package/tests/fixtures/gap2_minimal_repro.docx +0 -0
- package/tests/fixtures/generate_fixtures.py +69 -0
- package/tsup.config.ts +24 -0
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
5
5
|
import { basename, resolve, extname, dirname, join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import {
|
|
8
|
-
registerAppTool,
|
|
8
|
+
registerAppTool as origRegisterAppTool,
|
|
9
9
|
registerAppResource,
|
|
10
10
|
RESOURCE_MIME_TYPE,
|
|
11
11
|
} from "@modelcontextprotocol/ext-apps/server";
|
|
@@ -13,6 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import {
|
|
14
14
|
identifyEngine,
|
|
15
15
|
extractTextFromBuffer,
|
|
16
|
+
_extractTextFromDoc,
|
|
16
17
|
DocumentObject,
|
|
17
18
|
RedlineEngine,
|
|
18
19
|
BatchValidationError,
|
|
@@ -39,7 +40,12 @@ function readFileBytesOrThrow(filePath: string): Buffer {
|
|
|
39
40
|
return readFileSync(filePath);
|
|
40
41
|
} catch (err: any) {
|
|
41
42
|
if (err.code === "ENOENT") {
|
|
42
|
-
throw new Error(
|
|
43
|
+
throw new Error(
|
|
44
|
+
`File not found: ${filePath}. Note: If you are running in a sandboxed/containerized environment, ` +
|
|
45
|
+
`the host application or MCP server may not have access to your local workspace files. ` +
|
|
46
|
+
`You can resolve this by installing Adeu directly inside your sandboxed environment using ` +
|
|
47
|
+
`'uv tool install adeu' and executing the commands via the CLI.`
|
|
48
|
+
);
|
|
43
49
|
}
|
|
44
50
|
throw err;
|
|
45
51
|
}
|
|
@@ -74,12 +80,38 @@ const PROCESS_BATCH_OPERATIONS_DESC =
|
|
|
74
80
|
const DIFF_DOCX_DESC =
|
|
75
81
|
"Compares two DOCX files and returns a unified diff of their text content. Useful for analyzing differences between versions before editing.";
|
|
76
82
|
|
|
83
|
+
const gitSha = process.env.GIT_SHA || "unknown";
|
|
84
|
+
const buildTs = process.env.BUILD_TIMESTAMP || "unknown";
|
|
85
|
+
const packageVersion = process.env.PACKAGE_VERSION || "unknown";
|
|
86
|
+
const buildTag = ` [Adeu v${packageVersion}+${gitSha}]`;
|
|
87
|
+
|
|
77
88
|
// --- Server Setup ---
|
|
78
89
|
const server = new McpServer({
|
|
79
90
|
name: "adeu-redlining-service",
|
|
80
|
-
version:
|
|
91
|
+
version: packageVersion,
|
|
81
92
|
});
|
|
82
93
|
|
|
94
|
+
// Wrap server.registerTool to inject buildTag into descriptions
|
|
95
|
+
const originalRegisterTool = server.registerTool.bind(server);
|
|
96
|
+
server.registerTool = (name: string, schema: any, handler?: any) => {
|
|
97
|
+
if (schema && typeof schema === "object") {
|
|
98
|
+
if (schema.description) {
|
|
99
|
+
schema.description = schema.description.trim() + buildTag;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return originalRegisterTool(name, schema, handler);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Wrap registerAppTool to inject buildTag into descriptions
|
|
106
|
+
const registerAppTool: typeof origRegisterAppTool = (mcpServer, name, schema, handler) => {
|
|
107
|
+
if (schema && typeof schema === "object") {
|
|
108
|
+
if (schema.description) {
|
|
109
|
+
schema.description = schema.description.trim() + buildTag;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return origRegisterAppTool(mcpServer, name, schema, handler);
|
|
113
|
+
};
|
|
114
|
+
|
|
83
115
|
// Common CSP allowing Google Fonts used by Adeu UI templates
|
|
84
116
|
const UI_CSP = {
|
|
85
117
|
connectDomains: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"],
|
|
@@ -205,22 +237,31 @@ registerAppTool(
|
|
|
205
237
|
}) => {
|
|
206
238
|
try {
|
|
207
239
|
const buf = readFileBytesOrThrow(file_path);
|
|
208
|
-
const text = await extractTextFromBuffer(buf, clean_view);
|
|
209
240
|
|
|
210
241
|
if (mode === "outline") {
|
|
211
242
|
const doc = await DocumentObject.load(buf);
|
|
212
|
-
|
|
243
|
+
const extract_res = _extractTextFromDoc(doc, clean_view, true, true) as {
|
|
244
|
+
text: string;
|
|
245
|
+
paragraph_offsets: Map<any, [number, number]>;
|
|
246
|
+
};
|
|
247
|
+
const res = build_outline_response(
|
|
213
248
|
doc,
|
|
214
|
-
text,
|
|
249
|
+
extract_res.text,
|
|
215
250
|
file_path,
|
|
216
251
|
outline_max_level,
|
|
217
252
|
outline_verbose,
|
|
218
|
-
|
|
253
|
+
extract_res.paragraph_offsets,
|
|
254
|
+
);
|
|
255
|
+
return res as any;
|
|
219
256
|
}
|
|
257
|
+
|
|
258
|
+
const text = await extractTextFromBuffer(buf, clean_view);
|
|
220
259
|
if (mode === "appendix") {
|
|
221
|
-
|
|
260
|
+
const res = build_appendix_response(text, page, file_path);
|
|
261
|
+
return res as any;
|
|
222
262
|
}
|
|
223
|
-
|
|
263
|
+
const res = build_paginated_response(text, page, file_path);
|
|
264
|
+
return res as any;
|
|
224
265
|
} catch (e: any) {
|
|
225
266
|
return {
|
|
226
267
|
isError: true,
|
|
@@ -235,6 +276,8 @@ registerAppTool(
|
|
|
235
276
|
},
|
|
236
277
|
);
|
|
237
278
|
|
|
279
|
+
|
|
280
|
+
|
|
238
281
|
registerAppTool(
|
|
239
282
|
server,
|
|
240
283
|
"search_and_fetch_emails",
|
|
@@ -312,10 +355,18 @@ server.registerTool(
|
|
|
312
355
|
.boolean()
|
|
313
356
|
.optional()
|
|
314
357
|
.default(false)
|
|
315
|
-
.describe(
|
|
358
|
+
.describe(
|
|
359
|
+
"If True, simulates the changes and returns a detailed preview report without modifying any files.",
|
|
360
|
+
),
|
|
316
361
|
},
|
|
317
362
|
},
|
|
318
|
-
async ({
|
|
363
|
+
async ({
|
|
364
|
+
original_docx_path,
|
|
365
|
+
author_name,
|
|
366
|
+
changes,
|
|
367
|
+
output_path,
|
|
368
|
+
dry_run,
|
|
369
|
+
}) => {
|
|
319
370
|
try {
|
|
320
371
|
if (!author_name || !author_name.trim())
|
|
321
372
|
return {
|
|
@@ -547,7 +598,15 @@ server.registerTool(
|
|
|
547
598
|
);
|
|
548
599
|
server.registerTool(
|
|
549
600
|
"login_to_adeu_cloud",
|
|
550
|
-
{
|
|
601
|
+
{
|
|
602
|
+
description:
|
|
603
|
+
"Logs the user into Adeu Cloud. Opens a browser window for SSO authentication.\n\n" +
|
|
604
|
+
"IMPORTANT — login is user-level, not account-level:\n" +
|
|
605
|
+
"- An Adeu user can have multiple linked provider accounts (Microsoft, Google) and multiple mailboxes (personal + shared/delegated). One linked account is marked primary.\n" +
|
|
606
|
+
"- Signing in through ANY of the user's linked accounts authenticates the same Adeu user. Once logged in, the session can read from and draft in ALL of that user's linked accounts and ALL of their mailboxes — not just the one used to sign in.\n" +
|
|
607
|
+
"- The choice of which provider account to sign in through is purely an SSO mechanism; it does not select a 'current account' for the session.\n\n" +
|
|
608
|
+
"When the user asks which accounts or mailboxes are available, call `list_available_mailboxes` rather than naming a single account from the login response.",
|
|
609
|
+
},
|
|
551
610
|
async () => {
|
|
552
611
|
try {
|
|
553
612
|
return (await login_to_adeu_cloud()) as any;
|
|
@@ -605,9 +664,9 @@ server.registerTool(
|
|
|
605
664
|
"list_available_mailboxes",
|
|
606
665
|
{
|
|
607
666
|
description:
|
|
608
|
-
"Lists all personal and shared
|
|
609
|
-
"
|
|
610
|
-
"Omitting `mailbox_address` on those tools targets the user's primary personal mailbox.",
|
|
667
|
+
"Lists all personal and shared/delegated mailboxes the authenticated Adeu user has access to, across ALL of their linked provider accounts. Returns each mailbox's `email_address`, `display_name`, auto-processing settings, and write-back preference.\n\n" +
|
|
668
|
+
"This is the right tool to answer 'which accounts/mailboxes am I logged into?' — Adeu login is user-level, so a single MCP session can see every mailbox listed here regardless of which provider account was used for SSO.\n\n" +
|
|
669
|
+
"Call this FIRST when the user names a specific mailbox or shared inbox, to resolve the canonical `email_address`. Then pass that address as `mailbox_address` to `search_and_fetch_emails` or `create_email_draft` to scope the operation. Omitting `mailbox_address` on those tools targets the user's primary personal mailbox.",
|
|
611
670
|
inputSchema: {},
|
|
612
671
|
},
|
|
613
672
|
async () => {
|
|
@@ -637,7 +696,8 @@ export function formatBatchResult(
|
|
|
637
696
|
res += "\nDetailed Edit Reports:\n";
|
|
638
697
|
for (let i = 0; i < stats.edits.length; i++) {
|
|
639
698
|
const report = stats.edits[i];
|
|
640
|
-
const status_indicator =
|
|
699
|
+
const status_indicator =
|
|
700
|
+
report.status === "applied" ? "✅ [applied]" : "❌ [failed]";
|
|
641
701
|
res += `Edit ${i + 1} ${status_indicator}:\n`;
|
|
642
702
|
res += ` Target: '${report.target_text}'\n`;
|
|
643
703
|
res += ` New text: '${report.new_text}'\n`;
|
|
@@ -666,8 +726,10 @@ export function formatBatchResult(
|
|
|
666
726
|
async function main() {
|
|
667
727
|
const transport = new StdioServerTransport();
|
|
668
728
|
await server.connect(transport);
|
|
729
|
+
const gitSha = process.env.GIT_SHA || "unknown";
|
|
730
|
+
const buildTs = process.env.BUILD_TIMESTAMP || "unknown";
|
|
669
731
|
console.error(
|
|
670
|
-
`Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio`,
|
|
732
|
+
`Adeu MCP Server (Node.js Engine: ${identifyEngine()}) running on stdio build=${gitSha}@${buildTs}`,
|
|
671
733
|
);
|
|
672
734
|
}
|
|
673
735
|
|
package/src/mcp.bugs.test.ts
CHANGED
|
@@ -157,6 +157,8 @@ describe("Resolved Bugs MCP Server Verification", () => {
|
|
|
157
157
|
expect(res.result.content[0].text).toContain(
|
|
158
158
|
"Error executing tool read_docx: File not found:",
|
|
159
159
|
);
|
|
160
|
+
expect(res.result.content[0].text).toContain("sandboxed/containerized environment");
|
|
161
|
+
expect(res.result.content[0].text).toContain("uv tool install adeu");
|
|
160
162
|
expect(res.result.content[0].text).not.toContain("ENOENT"); // Raw node error must not leak
|
|
161
163
|
});
|
|
162
164
|
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// FILE: node/packages/mcp-server/src/parity_live.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 { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
describe("Parity Live Server Integration Verification", () => {
|
|
8
|
+
let serverProc: ChildProcess;
|
|
9
|
+
const fixturePath = resolve(__dirname, "../tests/fixtures/gap2_minimal_repro.docx");
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
13
|
+
if (!existsSync(serverPath)) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"MCP server not built. Run 'npm run build' before tests.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (!existsSync(fixturePath)) {
|
|
19
|
+
throw new Error(`Fixture not found: ${fixturePath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Spawn server process
|
|
23
|
+
serverProc = spawn("node", [serverPath]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
if (serverProc && !serverProc.killed) {
|
|
28
|
+
serverProc.kill();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Helper to interact with the stdio JSON-RPC server
|
|
33
|
+
function sendRpc(method: string, params: any, id: number = 1): Promise<any> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const timeout = setTimeout(() => reject(new Error("RPC Timeout")), 5000);
|
|
36
|
+
|
|
37
|
+
const listener = (data: Buffer) => {
|
|
38
|
+
const lines = data.toString().trim().split("\n");
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
if (!line.startsWith("{")) continue;
|
|
41
|
+
try {
|
|
42
|
+
const res = JSON.parse(line);
|
|
43
|
+
if (res.id === id) {
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
serverProc.stdout?.removeListener("data", listener);
|
|
46
|
+
resolve(res);
|
|
47
|
+
}
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Ignore incomplete chunks
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
serverProc.stdout?.on("data", listener);
|
|
55
|
+
serverProc.stdin?.write(
|
|
56
|
+
JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
it("does not expose the server_info tool anymore", async () => {
|
|
62
|
+
const listRes = await sendRpc("tools/list", {}, 200);
|
|
63
|
+
expect(listRes.result).toBeDefined();
|
|
64
|
+
const tools = listRes.result.tools || [];
|
|
65
|
+
const serverInfoTool = tools.find((t: any) => t.name === "server_info");
|
|
66
|
+
expect(serverInfoTool).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("exposes the build stamp via tool-call descriptions", async () => {
|
|
70
|
+
const listRes = await sendRpc("tools/list", {}, 201);
|
|
71
|
+
expect(listRes.result).toBeDefined();
|
|
72
|
+
const tools = listRes.result.tools || [];
|
|
73
|
+
const readDocx = tools.find((t: any) => t.name === "read_docx");
|
|
74
|
+
expect(readDocx).toBeDefined();
|
|
75
|
+
expect(readDocx.description).toContain("[Adeu v");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
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);
|
|
84
|
+
expect(initRes.result).toBeDefined();
|
|
85
|
+
expect(initRes.result.serverInfo).toBeDefined();
|
|
86
|
+
expect(initRes.result.serverInfo.version).not.toBe("1.0.0");
|
|
87
|
+
expect(initRes.result.serverInfo.version).not.toContain("unknown");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
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",
|
|
97
|
+
},
|
|
98
|
+
}, 202);
|
|
99
|
+
|
|
100
|
+
expect(res.result).toBeDefined();
|
|
101
|
+
expect(res.result.content[0].text).not.toContain("[Debug] build=");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
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,
|
|
121
|
+
},
|
|
122
|
+
}, 203);
|
|
123
|
+
|
|
124
|
+
expect(res.result).toBeDefined();
|
|
125
|
+
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.");
|
|
128
|
+
expect(text).not.toContain("Target text not found in document.");
|
|
129
|
+
|
|
130
|
+
// Run Python engine to assert cross-engine exact error parity
|
|
131
|
+
const { execSync } = await import("node:child_process");
|
|
132
|
+
const projectRoot = resolve(__dirname, "../../../..");
|
|
133
|
+
const pythonCli = join(projectRoot, "python/.venv/bin/adeu");
|
|
134
|
+
|
|
135
|
+
// Create temporary changes JSON for Python CLI
|
|
136
|
+
const fs = await import("node:fs");
|
|
137
|
+
const tempJsonPath = join(projectRoot, "tmp", `changes_${Date.now()}.json`);
|
|
138
|
+
fs.mkdirSync(join(projectRoot, "tmp"), { recursive: true });
|
|
139
|
+
fs.writeFileSync(tempJsonPath, JSON.stringify([
|
|
140
|
+
{
|
|
141
|
+
type: "modify",
|
|
142
|
+
target_text: "Foo bar old phrase here.",
|
|
143
|
+
new_text: "X",
|
|
144
|
+
comment: "c",
|
|
145
|
+
}
|
|
146
|
+
]));
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const pythonOut = execSync(`"${pythonCli}" apply "${fixturePath}" "${tempJsonPath}" --dry-run --author Ed`, {
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
});
|
|
152
|
+
// Should not succeed normally since dry run exits with code 1 on errors, so we catch in the try block
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
const pyErrorOutput = err.stdout || err.stderr || "";
|
|
155
|
+
expect(pyErrorOutput).toContain("Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.");
|
|
156
|
+
expect(pyErrorOutput).toContain("Reject/accept that change first or target the active replacement text instead.");
|
|
157
|
+
} finally {
|
|
158
|
+
if (fs.existsSync(tempJsonPath)) {
|
|
159
|
+
fs.unlinkSync(tempJsonPath);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("GAP 1 (Live): read_docx mode=outline heading count and content parity with Python", async () => {
|
|
165
|
+
const gap1FixturePath = resolve(__dirname, "../tests/fixtures/gap1_deleted_row_repro.docx");
|
|
166
|
+
const { execSync } = await import("node:child_process");
|
|
167
|
+
const projectRoot = resolve(__dirname, "../../../..");
|
|
168
|
+
const pythonCli = join(projectRoot, "python/.venv/bin/adeu");
|
|
169
|
+
|
|
170
|
+
// 1. clean_view = true
|
|
171
|
+
const nodeResClean = await sendRpc("tools/call", {
|
|
172
|
+
name: "read_docx",
|
|
173
|
+
arguments: {
|
|
174
|
+
file_path: gap1FixturePath,
|
|
175
|
+
mode: "outline",
|
|
176
|
+
clean_view: true,
|
|
177
|
+
},
|
|
178
|
+
}, 204);
|
|
179
|
+
|
|
180
|
+
expect(nodeResClean.result).toBeDefined();
|
|
181
|
+
const nodeTextClean = nodeResClean.result.content[0].text;
|
|
182
|
+
|
|
183
|
+
// Get Python output for clean_view = true
|
|
184
|
+
const pythonOutClean = execSync(`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`, {
|
|
185
|
+
encoding: "utf-8",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Extract raw markdown headings (lines starting with #) to assert perfect parity
|
|
189
|
+
const getHeadings = (text: string) => text.split("\n").filter(line => line.startsWith("#")).map(l => l.trim());
|
|
190
|
+
|
|
191
|
+
const nodeHeadingsClean = getHeadings(nodeTextClean);
|
|
192
|
+
const pythonHeadingsClean = getHeadings(pythonOutClean);
|
|
193
|
+
|
|
194
|
+
expect(nodeHeadingsClean).toEqual(["# Active Heading (p1)"]);
|
|
195
|
+
expect(pythonHeadingsClean).toEqual(["# Active Heading (p1)"]);
|
|
196
|
+
expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
|
|
197
|
+
|
|
198
|
+
// 2. clean_view = false
|
|
199
|
+
const nodeResDirty = await sendRpc("tools/call", {
|
|
200
|
+
name: "read_docx",
|
|
201
|
+
arguments: {
|
|
202
|
+
file_path: gap1FixturePath,
|
|
203
|
+
mode: "outline",
|
|
204
|
+
clean_view: false,
|
|
205
|
+
},
|
|
206
|
+
}, 205);
|
|
207
|
+
|
|
208
|
+
expect(nodeResDirty.result).toBeDefined();
|
|
209
|
+
const nodeTextDirty = nodeResDirty.result.content[0].text;
|
|
210
|
+
|
|
211
|
+
const pythonOutDirty = execSync(`"${pythonCli}" extract "${gap1FixturePath}" --mode outline`, {
|
|
212
|
+
encoding: "utf-8",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const nodeHeadingsDirty = getHeadings(nodeTextDirty);
|
|
216
|
+
const pythonHeadingsDirty = getHeadings(pythonOutDirty);
|
|
217
|
+
|
|
218
|
+
expect(nodeHeadingsDirty).toEqual(["# Active Heading (p1)", "# Deleted Heading (p1)"]);
|
|
219
|
+
expect(pythonHeadingsDirty).toEqual(["# Active Heading (p1)", "# Deleted Heading (p1)"]);
|
|
220
|
+
expect(nodeHeadingsDirty).toEqual(pythonHeadingsDirty);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("GAP 1 - Style Def (Live): style-definition outlineLvl on non-heading style classification parity", async () => {
|
|
224
|
+
const gap1FixturePath = resolve(__dirname, "../tests/fixtures/gap1_minimal_repro.docx");
|
|
225
|
+
const { execSync } = await import("node:child_process");
|
|
226
|
+
const projectRoot = resolve(__dirname, "../../../..");
|
|
227
|
+
const pythonCli = join(projectRoot, "python/.venv/bin/adeu");
|
|
228
|
+
|
|
229
|
+
// 1. clean_view = true (both engines must return exactly 2 headings, excluding Subtitle)
|
|
230
|
+
const nodeResClean = await sendRpc("tools/call", {
|
|
231
|
+
name: "read_docx",
|
|
232
|
+
arguments: {
|
|
233
|
+
file_path: gap1FixturePath,
|
|
234
|
+
mode: "outline",
|
|
235
|
+
clean_view: true,
|
|
236
|
+
},
|
|
237
|
+
}, 206);
|
|
238
|
+
|
|
239
|
+
expect(nodeResClean.result).toBeDefined();
|
|
240
|
+
const nodeTextClean = nodeResClean.result.content[0].text;
|
|
241
|
+
|
|
242
|
+
const pythonOutClean = execSync(`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`, {
|
|
243
|
+
encoding: "utf-8",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const getHeadings = (text: string) => text.split("\n").filter(line => line.startsWith("#")).map(l => l.trim());
|
|
247
|
+
|
|
248
|
+
const nodeHeadingsClean = getHeadings(nodeTextClean);
|
|
249
|
+
const pythonHeadingsClean = getHeadings(pythonOutClean);
|
|
250
|
+
|
|
251
|
+
// Assert exact count parity (must be exactly 2 for both)
|
|
252
|
+
expect(nodeHeadingsClean.length).toBe(2);
|
|
253
|
+
expect(pythonHeadingsClean.length).toBe(2);
|
|
254
|
+
|
|
255
|
+
expect(nodeHeadingsClean).toEqual(["# Real Heading One (p1)", "# Real Heading Two (p1)"]);
|
|
256
|
+
expect(pythonHeadingsClean).toEqual(["# Real Heading One (p1)", "# Real Heading Two (p1)"]);
|
|
257
|
+
expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
|
|
258
|
+
});
|
|
259
|
+
});
|
package/src/response-builders.ts
CHANGED
|
@@ -110,6 +110,7 @@ export function build_outline_response(
|
|
|
110
110
|
file_path: string,
|
|
111
111
|
outline_max_level: number = 2,
|
|
112
112
|
outline_verbose: boolean = false,
|
|
113
|
+
paragraph_offsets: Map<any, [number, number]> | null = null,
|
|
113
114
|
): ToolResult {
|
|
114
115
|
const [body] = split_structural_appendix(projected_text);
|
|
115
116
|
const pagination_result = paginate(body, "");
|
|
@@ -119,6 +120,7 @@ export function build_outline_response(
|
|
|
119
120
|
body,
|
|
120
121
|
pagination_result.body_pages,
|
|
121
122
|
pagination_result.body_page_offsets,
|
|
123
|
+
paragraph_offsets,
|
|
122
124
|
);
|
|
123
125
|
|
|
124
126
|
const rendered = render_outline_tree(
|
package/src/tools/auth.ts
CHANGED
|
@@ -24,11 +24,18 @@ export async function login_to_adeu_cloud(): Promise<ToolResult> {
|
|
|
24
24
|
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
|
25
25
|
|
|
26
26
|
const data: any = await res.json();
|
|
27
|
+
const email = data.email || "Unknown Email";
|
|
27
28
|
return {
|
|
28
29
|
content: [
|
|
29
30
|
{
|
|
30
31
|
type: "text",
|
|
31
|
-
text:
|
|
32
|
+
text:
|
|
33
|
+
`Login successful. You are now authenticated to Adeu Cloud as the user ` +
|
|
34
|
+
`who owns the provider account \`${email}\` (the account used for SSO).\n\n` +
|
|
35
|
+
`This single login grants access to ALL of this user's linked provider ` +
|
|
36
|
+
`accounts and ALL of their mailboxes for the duration of this session — ` +
|
|
37
|
+
`not just \`${email}\`. Call \`list_available_mailboxes\` to see every mailbox ` +
|
|
38
|
+
`that can be queried or drafted from.`,
|
|
32
39
|
},
|
|
33
40
|
],
|
|
34
41
|
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from docx import Document
|
|
3
|
+
from docx.oxml.ns import qn
|
|
4
|
+
from docx.oxml import OxmlElement
|
|
5
|
+
|
|
6
|
+
os.makedirs('node/packages/mcp-server/tests/fixtures', exist_ok=True)
|
|
7
|
+
|
|
8
|
+
doc = Document()
|
|
9
|
+
doc.add_paragraph("Section One").style = doc.styles['Heading 1']
|
|
10
|
+
p = doc.add_paragraph()
|
|
11
|
+
|
|
12
|
+
def run(text):
|
|
13
|
+
r = OxmlElement('w:r')
|
|
14
|
+
t = OxmlElement('w:t')
|
|
15
|
+
t.text = text
|
|
16
|
+
t.set(qn('xml:space'), 'preserve')
|
|
17
|
+
r.append(t)
|
|
18
|
+
return r
|
|
19
|
+
|
|
20
|
+
p._p.append(run("Foo bar "))
|
|
21
|
+
|
|
22
|
+
d = OxmlElement('w:del')
|
|
23
|
+
d.set(qn('w:id'), '10')
|
|
24
|
+
d.set(qn('w:author'), 'Test Negotiator')
|
|
25
|
+
d.set(qn('w:date'), '2026-01-22T12:06:55Z')
|
|
26
|
+
|
|
27
|
+
rd = OxmlElement('w:r')
|
|
28
|
+
dt = OxmlElement('w:delText')
|
|
29
|
+
dt.text = "old phrase here."
|
|
30
|
+
dt.set(qn('xml:space'), 'preserve')
|
|
31
|
+
rd.append(dt)
|
|
32
|
+
d.append(rd)
|
|
33
|
+
p._p.append(d)
|
|
34
|
+
|
|
35
|
+
ins = OxmlElement('w:ins')
|
|
36
|
+
ins.set(qn('w:id'), '11')
|
|
37
|
+
ins.set(qn('w:author'), 'Test Negotiator')
|
|
38
|
+
ins.set(qn('w:date'), '2026-01-22T12:06:55Z')
|
|
39
|
+
ins.append(run("new phrase here."))
|
|
40
|
+
p._p.append(ins)
|
|
41
|
+
|
|
42
|
+
doc.save('node/packages/mcp-server/tests/fixtures/gap2_minimal_repro.docx')
|
|
43
|
+
print("Successfully generated node/packages/mcp-server/tests/fixtures/gap2_minimal_repro.docx")
|
|
44
|
+
|
|
45
|
+
# Generate GAP 1 deleted row fixture
|
|
46
|
+
doc1 = Document()
|
|
47
|
+
doc1.add_paragraph("Active Heading").style = doc1.styles['Heading 1']
|
|
48
|
+
|
|
49
|
+
# Add a table
|
|
50
|
+
table = doc1.add_table(rows=1, cols=1)
|
|
51
|
+
row = table.rows[0]
|
|
52
|
+
cell = row.cells[0]
|
|
53
|
+
|
|
54
|
+
# Add a paragraph inside the cell with Heading 1 style
|
|
55
|
+
p_cell = cell.paragraphs[0]
|
|
56
|
+
p_cell.style = doc1.styles['Heading 1']
|
|
57
|
+
# Clear default text and append the text
|
|
58
|
+
p_cell.text = "Deleted Heading"
|
|
59
|
+
|
|
60
|
+
# Now let's mark the row as deleted (w:del inside w:trPr)
|
|
61
|
+
trPr = row._tr.get_or_add_trPr()
|
|
62
|
+
del_node = OxmlElement('w:del')
|
|
63
|
+
del_node.set(qn('w:id'), '100')
|
|
64
|
+
del_node.set(qn('w:author'), 'Test Negotiator')
|
|
65
|
+
del_node.set(qn('w:date'), '2026-01-22T12:06:55Z')
|
|
66
|
+
trPr.append(del_node)
|
|
67
|
+
|
|
68
|
+
doc1.save('node/packages/mcp-server/tests/fixtures/gap1_deleted_row_repro.docx')
|
|
69
|
+
print("Successfully generated node/packages/mcp-server/tests/fixtures/gap1_deleted_row_repro.docx")
|
package/tsup.config.ts
CHANGED
|
@@ -18,6 +18,20 @@ function copyAssets(outDir: string) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
|
|
23
|
+
const packageVersion = packageJson.version;
|
|
24
|
+
|
|
25
|
+
import { execSync } from "node:child_process";
|
|
26
|
+
|
|
27
|
+
let gitSha = "unknown";
|
|
28
|
+
try {
|
|
29
|
+
gitSha = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// fallback if not in git repo or git not found
|
|
32
|
+
}
|
|
33
|
+
const buildTimestamp = new Date().toISOString();
|
|
34
|
+
|
|
21
35
|
export default defineConfig([
|
|
22
36
|
{
|
|
23
37
|
entry: ["src/index.ts"],
|
|
@@ -29,6 +43,11 @@ export default defineConfig([
|
|
|
29
43
|
banner: {
|
|
30
44
|
js: "#!/usr/bin/env node",
|
|
31
45
|
},
|
|
46
|
+
define: {
|
|
47
|
+
"process.env.GIT_SHA": JSON.stringify(gitSha),
|
|
48
|
+
"process.env.BUILD_TIMESTAMP": JSON.stringify(buildTimestamp),
|
|
49
|
+
"process.env.PACKAGE_VERSION": JSON.stringify(packageVersion),
|
|
50
|
+
},
|
|
32
51
|
onSuccess: async () => {
|
|
33
52
|
copyAssets("dist");
|
|
34
53
|
},
|
|
@@ -43,6 +62,11 @@ export default defineConfig([
|
|
|
43
62
|
dts: false,
|
|
44
63
|
sourcemap: false,
|
|
45
64
|
clean: false, // Don't clean the whole dir (preserves icon and manifest)
|
|
65
|
+
define: {
|
|
66
|
+
"process.env.GIT_SHA": JSON.stringify(gitSha),
|
|
67
|
+
"process.env.BUILD_TIMESTAMP": JSON.stringify(buildTimestamp),
|
|
68
|
+
"process.env.PACKAGE_VERSION": JSON.stringify(packageVersion),
|
|
69
|
+
},
|
|
46
70
|
onSuccess: async () => {
|
|
47
71
|
copyAssets("../../../desktop-extension");
|
|
48
72
|
},
|