@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.
@@ -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
- let realEmailId: string | undefined;
314
- try {
315
- realEmailId = args.email_id ? resolveEmailId(args.email_id) : undefined;
316
- } catch (err) {
317
- if (err instanceof StaleShortIdError) {
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
- isError: true,
320
- content: [{ type: "text", text: err.message }],
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
- const payload = {
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
- // Remove undefined fields
341
- Object.keys(payload).forEach(
342
- (k) => (payload as any)[k] === undefined && delete (payload as any)[k],
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
- let res: Response;
346
- try {
347
- res = await fetch(`${BACKEND_URL}/api/v1/emails/search`, {
348
- method: "POST",
349
- headers: {
350
- Authorization: `Bearer ${apiKey}`,
351
- "Content-Type": "application/json",
352
- },
353
- body: JSON.stringify(payload),
354
- signal: AbortSignal.timeout(45_000),
355
- });
356
- } catch (err) {
357
- if (isTimeoutError(err)) {
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
- "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.",
439
+ "Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate.",
360
440
  );
361
441
  }
362
- throw err;
363
- }
442
+ if (!res.ok)
443
+ throw new Error(formatBackendError(res.status, await res.text()));
364
444
 
365
- if (res.status === 401) {
366
- DesktopAuthManager.clearApiKey();
367
- throw new Error(
368
- "Authentication expired. Please call `login_to_adeu_cloud` to re-authenticate.",
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") {
@@ -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
  },