@adeu/mcp-server 1.19.1 → 1.21.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,127 +0,0 @@
1
- // FILE: node/packages/mcp-server/src/desktop-auth.ts
2
- import { createServer, Server } from "node:http";
3
- import { exec } from "node:child_process";
4
- import { homedir, platform } from "node:os";
5
- import { join } from "node:path";
6
- import {
7
- writeFileSync,
8
- readFileSync,
9
- mkdirSync,
10
- existsSync,
11
- rmSync,
12
- chmodSync,
13
- } from "node:fs";
14
- import { FRONTEND_URL } from "./shared.js";
15
-
16
- const ADEU_DIR = join(homedir(), ".adeu");
17
- const CRED_PATH = join(ADEU_DIR, "credentials.json");
18
-
19
- function openBrowser(url: string) {
20
- if (platform() === "darwin") exec(`open "${url}"`);
21
- else if (platform() === "win32") exec(`start "" "${url}"`);
22
- else exec(`xdg-open "${url}"`);
23
- }
24
-
25
- export class DesktopAuthManager {
26
- static getApiKey(): string | null {
27
- if (!existsSync(CRED_PATH)) return null;
28
- try {
29
- const data = JSON.parse(readFileSync(CRED_PATH, "utf-8"));
30
- return data.api_key || null;
31
- } catch {
32
- return null;
33
- }
34
- }
35
-
36
- static setApiKey(apiKey: string): void {
37
- if (!existsSync(ADEU_DIR)) {
38
- mkdirSync(ADEU_DIR, { recursive: true });
39
- }
40
- writeFileSync(CRED_PATH, JSON.stringify({ api_key: apiKey }));
41
- // Restrict read/write to the current user only (equivalent to 0o600)
42
- chmodSync(CRED_PATH, 0o600);
43
- }
44
-
45
- static clearApiKey(): void {
46
- if (existsSync(CRED_PATH)) {
47
- rmSync(CRED_PATH);
48
- }
49
- }
50
-
51
- static async authenticateInteractive(): Promise<string> {
52
- return new Promise((resolve, reject) => {
53
- let server: Server;
54
-
55
- const timeout = setTimeout(
56
- () => {
57
- if (server) server.close();
58
- reject(new Error("Authentication timed out after 5 minutes."));
59
- },
60
- 5 * 60 * 1000,
61
- );
62
-
63
- server = createServer((req, res) => {
64
- const url = new URL(req.url || "", `http://${req.headers.host}`);
65
-
66
- if (url.pathname === "/callback") {
67
- const apiKey = url.searchParams.get("api_key");
68
-
69
- res.writeHead(apiKey ? 200 : 400, { "Content-Type": "text/html" });
70
- const title = apiKey
71
- ? "Authentication Successful!"
72
- : "Authentication Failed";
73
- const text = apiKey
74
- ? "Your Adeu MCP server has been successfully authenticated. You can safely close this window and return to Claude."
75
- : "No API key received. Please try again.";
76
- const color = apiKey ? "#107c10" : "#d83b01";
77
-
78
- res.end(`
79
- <!DOCTYPE html><html><head><title>${title}</title>
80
- <style>body{font-family:sans-serif;text-align:center;padding:50px;background:#f3f2f1;}.container{background:white;padding:40px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1);max-width:500px;margin:0 auto;}h1{color:${color};}p{color:#605e5c;line-height:1.5;}</style>
81
- </head><body><div class="container"><h1>${title}</h1><p>${text}</p>
82
- <script>setTimeout(()=>window.close(), 3000);</script>
83
- </div></body></html>
84
- `);
85
-
86
- clearTimeout(timeout);
87
- // Allow response to send before closing server
88
- setTimeout(() => server.close(), 100);
89
-
90
- if (apiKey) {
91
- this.setApiKey(apiKey);
92
- resolve(apiKey);
93
- } else {
94
- reject(new Error("No API key received in callback."));
95
- }
96
- } else {
97
- res.writeHead(404);
98
- res.end();
99
- }
100
- });
101
-
102
- server.listen(0, "127.0.0.1", () => {
103
- const address = server.address();
104
- if (address && typeof address !== "string") {
105
- const authUrl = `${FRONTEND_URL}/login?desktop_port=${address.port}`;
106
- openBrowser(authUrl);
107
- }
108
- });
109
- });
110
- }
111
-
112
- static async ensureAuthenticated(): Promise<string> {
113
- const key = this.getApiKey();
114
- if (key) return key;
115
- return this.authenticateInteractive();
116
- }
117
- }
118
-
119
- export async function getCloudAuthToken(): Promise<string> {
120
- const key = DesktopAuthManager.getApiKey();
121
- if (!key) {
122
- throw new Error(
123
- "Authentication Required: You are not logged in. Please call the `login_to_adeu_cloud` tool first to authenticate, then try this task again.",
124
- );
125
- }
126
- return key;
127
- }
@@ -1,152 +0,0 @@
1
- // FILE: node/packages/mcp-server/src/mcp.cloud.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 {
7
- existsSync,
8
- mkdirSync,
9
- readFileSync,
10
- writeFileSync,
11
- rmSync,
12
- } from "node:fs";
13
-
14
- describe("Cloud Auth & Email Tools MCP Verification", () => {
15
- let serverProc: ChildProcess;
16
- let testHomeDir: string;
17
- let adeuConfigDir: string;
18
- let credPath: string;
19
-
20
- beforeAll(async () => {
21
- // 1. Create a sandboxed home directory so we don't nuke the dev's actual login
22
- testHomeDir = join(tmpdir(), `adeu_test_home_${Date.now()}`);
23
- adeuConfigDir = join(testHomeDir, ".adeu");
24
- credPath = join(adeuConfigDir, "credentials.json");
25
- mkdirSync(testHomeDir, { recursive: true });
26
-
27
- // 2. Boot the compiled MCP server with the sandboxed HOME environment
28
- const serverPath = resolve(__dirname, "../dist/index.js");
29
- if (!existsSync(serverPath)) {
30
- throw new Error(
31
- "MCP server not built. Run 'npm run build' before tests.",
32
- );
33
- }
34
-
35
- serverProc = spawn("node", [serverPath], {
36
- env: {
37
- ...process.env,
38
- HOME: testHomeDir, // Mock homedir() for macOS/Linux
39
- USERPROFILE: testHomeDir, // Mock homedir() for Windows
40
- },
41
- });
42
- });
43
-
44
- afterAll(() => {
45
- if (serverProc && !serverProc.killed) serverProc.kill();
46
- if (existsSync(testHomeDir)) {
47
- rmSync(testHomeDir, { recursive: true, force: true });
48
- }
49
- });
50
-
51
- // Helper to interact with the stdio JSON-RPC server
52
- function sendRpc(method: string, params: any, id: number = 1): Promise<any> {
53
- return new Promise((resolve, reject) => {
54
- const timeout = setTimeout(() => reject(new Error("RPC Timeout")), 5000);
55
-
56
- const listener = (data: Buffer) => {
57
- const lines = data.toString().trim().split("\n");
58
- for (const line of lines) {
59
- if (!line.startsWith("{")) continue;
60
- try {
61
- const res = JSON.parse(line);
62
- if (res.id === id) {
63
- clearTimeout(timeout);
64
- serverProc.stdout?.removeListener("data", listener);
65
- resolve(res);
66
- }
67
- } catch (e) {
68
- // Ignore incomplete chunks
69
- }
70
- }
71
- };
72
-
73
- serverProc.stdout?.on("data", listener);
74
- serverProc.stdin?.write(
75
- JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n",
76
- );
77
- });
78
- }
79
-
80
- it("CLOUD-1: Enforces authentication trap for search_and_fetch_emails", async () => {
81
- // Attempt to search emails without a credentials.json file in the sandbox
82
- const res = await sendRpc(
83
- "tools/call",
84
- {
85
- name: "search_and_fetch_emails",
86
- arguments: { reasoning: "test", subject: "Invoice" },
87
- },
88
- 201,
89
- );
90
-
91
- expect(res.result.isError).toBe(true);
92
- expect(res.result.content[0].text).toContain("Authentication Required");
93
- expect(res.result.content[0].text).toContain("login_to_adeu_cloud");
94
- });
95
- it("CLOUD-1b: Enforces authentication trap for list_available_mailboxes", async () => {
96
- const res = await sendRpc(
97
- "tools/call",
98
- {
99
- name: "list_available_mailboxes",
100
- arguments: { reasoning: "test" },
101
- },
102
- 204,
103
- );
104
-
105
- expect(res.result.isError).toBe(true);
106
- expect(res.result.content[0].text).toContain("Authentication Required");
107
- expect(res.result.content[0].text).toContain("login_to_adeu_cloud");
108
- });
109
- it("CLOUD-2: Validates create_email_draft missing required arguments", async () => {
110
- // To bypass the auth trap for this test, we inject a dummy credential file
111
- mkdirSync(adeuConfigDir, { recursive: true });
112
- writeFileSync(credPath, JSON.stringify({ api_key: "dummy_test_key" }));
113
-
114
- const res = await sendRpc(
115
- "tools/call",
116
- {
117
- name: "create_email_draft",
118
- arguments: {
119
- reasoning: "test",
120
- body_markdown: "Hello World",
121
- // Missing reply_to_email_id AND subject/to_recipients
122
- },
123
- },
124
- 202,
125
- );
126
-
127
- expect(res.result.isError).toBe(true);
128
- expect(res.result.content[0].text).toContain(
129
- "You must provide either 'reply_to_email_id' OR both 'subject' and 'to_recipients'",
130
- );
131
- });
132
-
133
- it("CLOUD-3: logout_of_adeu_cloud successfully deletes the credentials file", async () => {
134
- // Ensure the dummy file from the previous test exists
135
- expect(existsSync(credPath)).toBe(true);
136
-
137
- const res = await sendRpc(
138
- "tools/call",
139
- {
140
- name: "logout_of_adeu_cloud",
141
- arguments: { reasoning: "test" },
142
- },
143
- 203,
144
- );
145
-
146
- expect(res.result.isError).toBeFalsy();
147
- expect(res.result.content[0].text).toContain("Successfully logged out");
148
-
149
- // Verify the file was physically deleted from the sandboxed .adeu directory
150
- expect(existsSync(credPath)).toBe(false);
151
- });
152
- });