@adeu/mcp-server 1.8.0 → 1.10.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/src/tools/auth.ts CHANGED
@@ -12,6 +12,7 @@ export async function login_to_adeu_cloud(): Promise<ToolResult> {
12
12
  Authorization: `Bearer ${apiKey}`,
13
13
  Accept: "application/json",
14
14
  },
15
+ signal: AbortSignal.timeout(15_000),
15
16
  });
16
17
 
17
18
  if (res.status === 401) {
@@ -0,0 +1,258 @@
1
+ // FILE: node/packages/mcp-server/src/tools/email.test.ts
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
+ import { search_and_fetch_emails, list_available_mailboxes } from "./email.js";
4
+
5
+ // Mock the Auth module so tests bypass active browser logins
6
+ vi.mock("../desktop-auth.js", () => {
7
+ return {
8
+ getCloudAuthToken: vi.fn().mockResolvedValue("mock_token_abc"),
9
+ DesktopAuthManager: {
10
+ getApiKey: vi.fn().mockReturnValue("mock_token_abc"),
11
+ clearApiKey: vi.fn(),
12
+ },
13
+ };
14
+ });
15
+
16
+ describe("Node Email Tools Finding #2 and Finding #6 tests", () => {
17
+ const originalFetch = global.fetch;
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ afterEach(() => {
24
+ global.fetch = originalFetch;
25
+ });
26
+
27
+ it("Finding #6: Correctly handles stale msg_ short IDs inside tool boundary", async () => {
28
+ const result = await search_and_fetch_emails({
29
+ email_id: "msg_stale99",
30
+ });
31
+
32
+ expect(result.isError).toBe(true);
33
+ const text = result.content[0].text;
34
+ expect(text).toContain("is not in the local cache");
35
+ expect(text).toContain("evicted, or it came from a different machine");
36
+ expect(text).toContain("Re-run search_and_fetch_emails with filters");
37
+ });
38
+
39
+ it("Finding #6: Maps backend Mailbox Not Found 404 error to Node boundary ToolError", async () => {
40
+ // Stub fetch to simulate a 404 response with Mailbox error body
41
+ global.fetch = vi.fn().mockResolvedValue({
42
+ ok: false,
43
+ status: 404,
44
+ text: async () =>
45
+ JSON.stringify({
46
+ detail: "Mailbox 'bogus@nowhere.invalid' not found.",
47
+ }),
48
+ } as Response);
49
+
50
+ await expect(
51
+ search_and_fetch_emails({ mailbox_address: "bogus@nowhere.invalid" }),
52
+ ).rejects.toThrowError(
53
+ "Cloud search failed (HTTP 404): The mailbox 'bogus@nowhere.invalid' is not connected to your Adeu account. Call list_available_mailboxes to see valid mailbox addresses, then retry with one of those as `mailbox_address`.",
54
+ );
55
+ });
56
+
57
+ it("Finding #6: Maps backend Email Not Found 404 error to Node boundary ToolError", async () => {
58
+ // Stub fetch to simulate 404 for an invalid adeu_ ID (which bypasses local cache checks)
59
+ global.fetch = vi.fn().mockResolvedValue({
60
+ ok: false,
61
+ status: 404,
62
+ text: async () => JSON.stringify({ detail: "Email not found." }),
63
+ } as Response);
64
+
65
+ await expect(
66
+ search_and_fetch_emails({ email_id: "adeu_9999" }),
67
+ ).rejects.toThrowError(
68
+ "Cloud search failed (HTTP 404): The email ID was not found. If this was a short ID (msg_*), it may have been evicted from the local cache or come from a different machine — re-run search_and_fetch_emails with filters to get a fresh ID. If it was an adeu_<numeric> or raw provider ID, verify it's correct.",
69
+ );
70
+ });
71
+
72
+ it("Finding #2: Asserts correct Markdown parity and Personal Mailbox fallback on list_available_mailboxes", async () => {
73
+ // Stub fetch to return one null-display mailbox and one alphabetical secondary mailbox
74
+ global.fetch = vi.fn().mockResolvedValue({
75
+ ok: true,
76
+ status: 200,
77
+ json: async () => [
78
+ {
79
+ email_address: "secondary@adeu.ai",
80
+ display_name: "Secondary Mailbox",
81
+ auto_process_enabled: false,
82
+ write_back_preference: "INTERNAL",
83
+ },
84
+ {
85
+ email_address: "primary@adeu.ai",
86
+ display_name: null, // Tests 'Personal Mailbox' fallback
87
+ auto_process_enabled: true,
88
+ write_back_preference: "DRAFT",
89
+ },
90
+ ],
91
+ } as Response);
92
+
93
+ const result = await list_available_mailboxes();
94
+ expect(result.isError).toBeFalsy();
95
+
96
+ const output = result.content[0].text;
97
+
98
+ // Verify preamble parity
99
+ expect(output).toContain("### Connected Mailboxes");
100
+ expect(output).toContain(
101
+ "Below is the list of connected mailboxes you have access to.",
102
+ );
103
+
104
+ // Verify Fallback formatting
105
+ expect(output).toContain("**Personal Mailbox**");
106
+
107
+ // Verify deterministic alphabetical sorting by email address (primary before secondary)
108
+ const idxPrimary = output.indexOf("primary@adeu.ai");
109
+ const idxSecondary = output.indexOf("secondary@adeu.ai");
110
+ expect(idxPrimary).not.toBe(-1);
111
+ expect(idxSecondary).not.toBe(-1);
112
+ expect(idxPrimary).toBeLessThan(idxSecondary);
113
+
114
+ // Verify labels formatting matches Python exactly
115
+ expect(output).toContain("- **Email Address**:");
116
+ expect(output).toContain("- **Auto-Processing**:");
117
+ expect(output).toContain("- **Write-Back Mode**:");
118
+ });
119
+
120
+ it("Finding #6: Gracefully maps generic, unmapped server errors on search_and_fetch_emails", async () => {
121
+ // Simulate generic HTTP 500 error
122
+ global.fetch = vi.fn().mockResolvedValue({
123
+ ok: false,
124
+ status: 500,
125
+ text: async () =>
126
+ JSON.stringify({ detail: "Database connection failed." }),
127
+ } as Response);
128
+
129
+ await expect(search_and_fetch_emails({ limit: 10 })).rejects.toThrowError(
130
+ "Cloud search failed (HTTP 500): Database connection failed.",
131
+ );
132
+ });
133
+
134
+ it("Finding #6: Converts abort/timeout errors to actionable ToolErrors on search_and_fetch_emails", async () => {
135
+ // Simulate a native fetch Timeout/AbortError
136
+ const timeoutError = new Error("The operation was aborted.");
137
+ timeoutError.name = "AbortError";
138
+ global.fetch = vi.fn().mockRejectedValue(timeoutError);
139
+
140
+ await expect(search_and_fetch_emails({ limit: 10 })).rejects.toThrowError(
141
+ "Email search timed out after 45s. The mail provider (Outlook/Gmail) may be slow.",
142
+ );
143
+ });
144
+
145
+ it("Findings #3, #9, and #11: Asserts preview pagination, auto-escalation note, and downstream tool suggests", async () => {
146
+ // --- Scenario 1: Previews listing with limit met (Finding #3 pagination hint) ---
147
+ const mockPreviewsPayload = {
148
+ type: "previews",
149
+ previews: [
150
+ {
151
+ id: "id1",
152
+ subject: "Subject 1",
153
+ sender_name: "Sender 1",
154
+ sender_email: "s1@adeu.ai",
155
+ received_datetime: "2026-01-01T12:00:00Z",
156
+ preview_text: "Text 1",
157
+ has_attachments: false,
158
+ is_read: true,
159
+ },
160
+ {
161
+ id: "id2",
162
+ subject: "Subject 2",
163
+ sender_name: "Sender 2",
164
+ sender_email: "s2@adeu.ai",
165
+ received_datetime: "2026-01-01T12:00:00Z",
166
+ preview_text: "Text 2",
167
+ has_attachments: false,
168
+ is_read: true,
169
+ },
170
+ ],
171
+ };
172
+
173
+ global.fetch = vi.fn().mockResolvedValue({
174
+ ok: true,
175
+ status: 200,
176
+ json: async () => mockPreviewsPayload,
177
+ } as Response);
178
+
179
+ const resPreviews = await search_and_fetch_emails({
180
+ subject: "Invoice",
181
+ limit: 2,
182
+ offset: 0,
183
+ });
184
+
185
+ const previewsText = resPreviews.content[0].text;
186
+ expect(previewsText).toContain(
187
+ "*(If you need to see more results, call this tool again with offset=2)*",
188
+ );
189
+
190
+ // --- Scenario 2: Single result auto-escalation (Finding #11 banner notice) ---
191
+ const mockFullEmailPayload = {
192
+ type: "full_email",
193
+ full_email: {
194
+ id: "adeu_12345",
195
+ subject: "Contract Review Required",
196
+ sender_name: "Legal",
197
+ sender_email: "legal@adeu.ai",
198
+ received_datetime: "2026-01-01T12:00:00Z",
199
+ body_html: "<p>Please look at this document.</p>",
200
+ is_thread: false,
201
+ attachments: [],
202
+ },
203
+ };
204
+
205
+ global.fetch = vi.fn().mockResolvedValue({
206
+ ok: true,
207
+ status: 200,
208
+ json: async () => mockFullEmailPayload,
209
+ } as Response);
210
+
211
+ const resEscalation = await search_and_fetch_emails({
212
+ subject: "Contract Review Required",
213
+ });
214
+
215
+ const escalationText = resEscalation.content[0].text;
216
+ expect(
217
+ escalationText.startsWith(
218
+ "_(Search returned exactly one result; auto-fetched full email below.)_",
219
+ ),
220
+ ).toBe(true);
221
+
222
+ // --- Scenario 3: Suggestions for attachments (Finding #9 downstream hint) ---
223
+ const mockAttachmentsPayload = {
224
+ type: "full_email",
225
+ full_email: {
226
+ id: "adeu_12345",
227
+ subject: "Contract Attachment",
228
+ sender_name: "Legal",
229
+ sender_email: "legal@adeu.ai",
230
+ received_datetime: "2026-01-01T12:00:00Z",
231
+ body_html: "<p>Please see attachment.</p>",
232
+ is_thread: false,
233
+ attachments: [
234
+ {
235
+ filename: "draft_contract.docx",
236
+ size_bytes: 1024,
237
+ base64_data: Buffer.from("dummy docx contents").toString("base64"),
238
+ },
239
+ ],
240
+ },
241
+ };
242
+
243
+ global.fetch = vi.fn().mockResolvedValue({
244
+ ok: true,
245
+ status: 200,
246
+ json: async () => mockAttachmentsPayload,
247
+ } as Response);
248
+
249
+ const resAttachments = await search_and_fetch_emails({
250
+ email_id: "adeu_12345",
251
+ });
252
+
253
+ const attachmentsText = resAttachments.content[0].text;
254
+ expect(attachmentsText).toContain(
255
+ "You can now use tools like `read_docx`, `diff_docx_files`, or `finalize_document`",
256
+ );
257
+ });
258
+ });