@adeu/mcp-server 1.20.0 → 1.22.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/dist/index.js +99 -1064
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +72 -236
- package/src/response-builders.ts +49 -1
- package/src/shared.ts +0 -5
- package/dist/templates/email_ui.html +0 -770
- package/src/desktop-auth.ts +0 -127
- package/src/mcp.cloud.test.ts +0 -152
- package/src/templates/email_ui.html +0 -770
- package/src/tools/auth.ts +0 -57
- package/src/tools/email.test.ts +0 -697
- package/src/tools/email.ts +0 -925
package/src/desktop-auth.ts
DELETED
|
@@ -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
|
-
}
|
package/src/mcp.cloud.test.ts
DELETED
|
@@ -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
|
-
});
|