@adeu/mcp-server 1.10.1 → 1.12.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/README.md +15 -1
- package/dist/index.js +160 -61
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/scripts/verify-bundle.js +50 -0
- package/src/index.test.ts +21 -2
- package/src/index.ts +59 -10
- package/src/mcp.bugs.test.ts +2 -0
- package/src/parity_live.test.ts +265 -0
- package/src/response-builders.ts +2 -0
- package/src/tools/email.test.ts +104 -1
- package/src/tools/email.ts +137 -51
- 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/tools/email.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// FILE: node/packages/mcp-server/src/tools/email.ts
|
|
2
1
|
import { homedir, tmpdir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -310,68 +309,155 @@ export async function search_and_fetch_emails(args: any): Promise<ToolResult> {
|
|
|
310
309
|
args.max_attachment_size_mb > 0
|
|
311
310
|
? args.max_attachment_size_mb
|
|
312
311
|
: 10;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
312
|
+
|
|
313
|
+
let data: any;
|
|
314
|
+
|
|
315
|
+
if (args.task_id) {
|
|
316
|
+
// ==========================================
|
|
317
|
+
// PHASE 2: POLL (Wait for completion)
|
|
318
|
+
// ==========================================
|
|
319
|
+
const pollUrl = `${BACKEND_URL}/api/v1/emails/tasks/${args.task_id}`;
|
|
320
|
+
let completedData: any = null;
|
|
321
|
+
|
|
322
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
323
|
+
let res: Response;
|
|
324
|
+
try {
|
|
325
|
+
res = await fetch(pollUrl, {
|
|
326
|
+
headers: {
|
|
327
|
+
Authorization: `Bearer ${apiKey}`,
|
|
328
|
+
Accept: "application/json",
|
|
329
|
+
},
|
|
330
|
+
signal: AbortSignal.timeout(15_000),
|
|
331
|
+
});
|
|
332
|
+
} catch (err) {
|
|
333
|
+
if (isTimeoutError(err)) {
|
|
334
|
+
throw new Error("Checking task status timed out.");
|
|
335
|
+
}
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (res.status === 401) {
|
|
340
|
+
DesktopAuthManager.clearApiKey();
|
|
341
|
+
throw new Error(
|
|
342
|
+
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
throw new Error(formatBackendError(res.status, await res.text()));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const taskData: any = await res.json();
|
|
350
|
+
const status = taskData.status;
|
|
351
|
+
|
|
352
|
+
if (status === "COMPLETED") {
|
|
353
|
+
completedData = taskData;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (status === "FAILED") {
|
|
358
|
+
const errorMsg = taskData.error || "Unknown internal error";
|
|
359
|
+
throw new Error(`Validation task failed on the server: ${errorMsg}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Wait 5 seconds before next poll
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!completedData) {
|
|
367
|
+
const msg = `Task ${args.task_id} is still processing. Please call \`search_and_fetch_emails\` again with task_id=${args.task_id}.`;
|
|
318
368
|
return {
|
|
319
|
-
|
|
320
|
-
|
|
369
|
+
content: [{ type: "text", text: msg }],
|
|
370
|
+
structuredContent: {
|
|
371
|
+
status: "pending",
|
|
372
|
+
task_id: args.task_id,
|
|
373
|
+
message: msg,
|
|
374
|
+
},
|
|
321
375
|
};
|
|
322
376
|
}
|
|
323
|
-
throw err;
|
|
324
|
-
}
|
|
325
377
|
|
|
326
|
-
|
|
327
|
-
email_id: realEmailId,
|
|
328
|
-
sender: args.sender,
|
|
329
|
-
subject: args.subject,
|
|
330
|
-
has_attachments: args.has_attachments,
|
|
331
|
-
attachment_name: args.attachment_name,
|
|
332
|
-
is_unread: args.is_unread,
|
|
333
|
-
days_ago: args.days_ago,
|
|
334
|
-
folder: args.folder,
|
|
335
|
-
limit: args.limit ?? 10,
|
|
336
|
-
offset: args.offset ?? 0,
|
|
337
|
-
mailbox_address: args.mailbox_address,
|
|
338
|
-
};
|
|
378
|
+
data = completedData;
|
|
339
379
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
380
|
+
} else {
|
|
381
|
+
// ==========================================
|
|
382
|
+
// PHASE 1: INIT / SEARCH (Search/Fetch standard)
|
|
383
|
+
// ==========================================
|
|
384
|
+
let realEmailId: string | undefined;
|
|
385
|
+
try {
|
|
386
|
+
realEmailId = args.email_id ? resolveEmailId(args.email_id) : undefined;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (err instanceof StaleShortIdError) {
|
|
389
|
+
return {
|
|
390
|
+
isError: true,
|
|
391
|
+
content: [{ type: "text", text: err.message }],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
344
396
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
397
|
+
const payload = {
|
|
398
|
+
email_id: realEmailId,
|
|
399
|
+
sender: args.sender,
|
|
400
|
+
subject: args.subject,
|
|
401
|
+
has_attachments: args.has_attachments,
|
|
402
|
+
attachment_name: args.attachment_name,
|
|
403
|
+
is_unread: args.is_unread,
|
|
404
|
+
days_ago: args.days_ago,
|
|
405
|
+
folder: args.folder,
|
|
406
|
+
limit: args.limit ?? 10,
|
|
407
|
+
offset: args.offset ?? 0,
|
|
408
|
+
mailbox_address: args.mailbox_address,
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Remove undefined fields
|
|
412
|
+
Object.keys(payload).forEach(
|
|
413
|
+
(k) => (payload as any)[k] === undefined && delete (payload as any)[k],
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
let res: Response;
|
|
417
|
+
try {
|
|
418
|
+
res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
|
|
419
|
+
method: "POST",
|
|
420
|
+
headers: {
|
|
421
|
+
Authorization: `Bearer ${apiKey}`,
|
|
422
|
+
"Content-Type": "application/json",
|
|
423
|
+
},
|
|
424
|
+
body: JSON.stringify(payload),
|
|
425
|
+
signal: AbortSignal.timeout(45_000),
|
|
426
|
+
});
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (isTimeoutError(err)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
"Email search timed out after 45s. The mail provider (Outlook/Gmail) may be slow. Try narrowing the search with more filters (sender, subject, days_ago), or retry shortly.",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (res.status === 401) {
|
|
437
|
+
DesktopAuthManager.clearApiKey();
|
|
358
438
|
throw new Error(
|
|
359
|
-
"
|
|
439
|
+
"Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate.",
|
|
360
440
|
);
|
|
361
441
|
}
|
|
362
|
-
|
|
363
|
-
|
|
442
|
+
if (!res.ok)
|
|
443
|
+
throw new Error(formatBackendError(res.status, await res.text()));
|
|
364
444
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
445
|
+
data = await res.json();
|
|
446
|
+
|
|
447
|
+
if (res.status === 202 || (data && (data.status === "pending" || data.task_id) && data.type === undefined)) {
|
|
448
|
+
const newTaskId = data.task_id;
|
|
449
|
+
const msg = `Email processing task started successfully. Task ID: ${newTaskId}. Please call \`search_and_fetch_emails\` again immediately with task_id=${newTaskId} to monitor the progress.`;
|
|
450
|
+
return {
|
|
451
|
+
content: [{ type: "text", text: msg }],
|
|
452
|
+
structuredContent: {
|
|
453
|
+
status: "pending",
|
|
454
|
+
task_id: String(newTaskId),
|
|
455
|
+
message: msg,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
370
459
|
}
|
|
371
|
-
if (!res.ok)
|
|
372
|
-
throw new Error(formatBackendError(res.status, await res.text()));
|
|
373
460
|
|
|
374
|
-
const data: any = await res.json();
|
|
375
461
|
const cache = loadIdCache();
|
|
376
462
|
|
|
377
463
|
if (data.type === "previews") {
|
|
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
|
},
|