@d-mato/gmail-mcp 0.1.1 → 0.1.4
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/build/gmail-client.js +1 -61
- package/build/tools/error-handler.js +20 -0
- package/build/tools/labels.js +2 -21
- package/build/tools/messages.js +3 -22
- package/build/utils.js +61 -0
- package/package.json +8 -6
package/build/gmail-client.js
CHANGED
|
@@ -1,65 +1,5 @@
|
|
|
1
1
|
import { google } from "googleapis";
|
|
2
|
-
|
|
3
|
-
const get = (name) => headers?.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ??
|
|
4
|
-
"";
|
|
5
|
-
return {
|
|
6
|
-
from: get("From"),
|
|
7
|
-
to: get("To"),
|
|
8
|
-
subject: get("Subject"),
|
|
9
|
-
date: get("Date"),
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
function decodeBase64Url(data) {
|
|
13
|
-
return Buffer.from(data, "base64url").toString("utf-8");
|
|
14
|
-
}
|
|
15
|
-
function extractTextBody(payload) {
|
|
16
|
-
// Direct body
|
|
17
|
-
if (payload.mimeType === "text/plain" && payload.body?.data) {
|
|
18
|
-
return decodeBase64Url(payload.body.data);
|
|
19
|
-
}
|
|
20
|
-
// Multipart — recurse
|
|
21
|
-
if (payload.parts) {
|
|
22
|
-
// Prefer text/plain
|
|
23
|
-
for (const part of payload.parts) {
|
|
24
|
-
if (part.mimeType === "text/plain" && part.body?.data) {
|
|
25
|
-
return decodeBase64Url(part.body.data);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// Fall back to text/html with tag stripping
|
|
29
|
-
for (const part of payload.parts) {
|
|
30
|
-
if (part.mimeType === "text/html" && part.body?.data) {
|
|
31
|
-
return stripHtml(decodeBase64Url(part.body.data));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
// Recurse into nested multipart
|
|
35
|
-
for (const part of payload.parts) {
|
|
36
|
-
const text = extractTextBody(part);
|
|
37
|
-
if (text)
|
|
38
|
-
return text;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// Fallback: HTML body at top level
|
|
42
|
-
if (payload.mimeType === "text/html" && payload.body?.data) {
|
|
43
|
-
return stripHtml(decodeBase64Url(payload.body.data));
|
|
44
|
-
}
|
|
45
|
-
return "";
|
|
46
|
-
}
|
|
47
|
-
function stripHtml(html) {
|
|
48
|
-
return html
|
|
49
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
50
|
-
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
51
|
-
.replace(/<br\s*\/?>/gi, "\n")
|
|
52
|
-
.replace(/<\/p>/gi, "\n\n")
|
|
53
|
-
.replace(/<[^>]+>/g, "")
|
|
54
|
-
.replace(/ /g, " ")
|
|
55
|
-
.replace(/&/g, "&")
|
|
56
|
-
.replace(/</g, "<")
|
|
57
|
-
.replace(/>/g, ">")
|
|
58
|
-
.replace(/"/g, '"')
|
|
59
|
-
.replace(/'/g, "'")
|
|
60
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
61
|
-
.trim();
|
|
62
|
-
}
|
|
2
|
+
import { extractHeaders, extractTextBody, } from "./utils.js";
|
|
63
3
|
export class GmailClient {
|
|
64
4
|
gmail;
|
|
65
5
|
constructor(auth) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function errorResponse(message) {
|
|
2
|
+
return {
|
|
3
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
4
|
+
isError: true,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function wrapGmailError(err, resourceName = "Resource") {
|
|
8
|
+
const error = err;
|
|
9
|
+
const code = error.code;
|
|
10
|
+
const msg = error.message ?? String(err);
|
|
11
|
+
if (code === 404)
|
|
12
|
+
return errorResponse(`${resourceName} not found`);
|
|
13
|
+
if (code === 400)
|
|
14
|
+
return errorResponse(`Invalid request: ${msg}`);
|
|
15
|
+
if (code === 403)
|
|
16
|
+
return errorResponse("Insufficient permissions");
|
|
17
|
+
if (code === 429)
|
|
18
|
+
return errorResponse("Rate limited — try again later");
|
|
19
|
+
return errorResponse(msg);
|
|
20
|
+
}
|
package/build/tools/labels.js
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
return {
|
|
4
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
5
|
-
isError: true,
|
|
6
|
-
};
|
|
7
|
-
}
|
|
8
|
-
function wrapGmailError(err) {
|
|
9
|
-
const error = err;
|
|
10
|
-
const code = error.code;
|
|
11
|
-
const msg = error.message ?? String(err);
|
|
12
|
-
if (code === 404)
|
|
13
|
-
return errorResponse("Label not found");
|
|
14
|
-
if (code === 400)
|
|
15
|
-
return errorResponse(`Invalid request: ${msg}`);
|
|
16
|
-
if (code === 403)
|
|
17
|
-
return errorResponse("Insufficient permissions");
|
|
18
|
-
if (code === 429)
|
|
19
|
-
return errorResponse("Rate limited — try again later");
|
|
20
|
-
return errorResponse(msg);
|
|
21
|
-
}
|
|
2
|
+
import { wrapGmailError } from "./error-handler.js";
|
|
22
3
|
export function registerLabelTools(server, gmail) {
|
|
23
4
|
server.tool("gmail_list_labels", "List all Gmail labels", {}, async () => {
|
|
24
5
|
try {
|
|
@@ -29,7 +10,7 @@ export function registerLabelTools(server, gmail) {
|
|
|
29
10
|
return { content: [{ type: "text", text: text || "No labels found." }] };
|
|
30
11
|
}
|
|
31
12
|
catch (err) {
|
|
32
|
-
return wrapGmailError(err);
|
|
13
|
+
return wrapGmailError(err, "Label");
|
|
33
14
|
}
|
|
34
15
|
});
|
|
35
16
|
server.tool("gmail_add_label", "Add a label to a message", {
|
package/build/tools/messages.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { wrapGmailError } from "./error-handler.js";
|
|
2
3
|
function formatMessageSummary(msg) {
|
|
3
4
|
return [
|
|
4
5
|
`ID: ${msg.id}`,
|
|
@@ -24,26 +25,6 @@ function formatMessageDetail(msg) {
|
|
|
24
25
|
msg.body,
|
|
25
26
|
].join("\n");
|
|
26
27
|
}
|
|
27
|
-
function errorResponse(message) {
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
30
|
-
isError: true,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function wrapGmailError(err) {
|
|
34
|
-
const error = err;
|
|
35
|
-
const code = error.code;
|
|
36
|
-
const msg = error.message ?? String(err);
|
|
37
|
-
if (code === 404)
|
|
38
|
-
return errorResponse("Message not found");
|
|
39
|
-
if (code === 400)
|
|
40
|
-
return errorResponse(`Invalid request: ${msg}`);
|
|
41
|
-
if (code === 403)
|
|
42
|
-
return errorResponse("Insufficient permissions");
|
|
43
|
-
if (code === 429)
|
|
44
|
-
return errorResponse("Rate limited — try again later");
|
|
45
|
-
return errorResponse(msg);
|
|
46
|
-
}
|
|
47
28
|
export function registerMessageTools(server, gmail) {
|
|
48
29
|
server.tool("gmail_search", "Search Gmail messages using Gmail query syntax (e.g. 'from:user@example.com', 'is:unread', 'subject:hello')", {
|
|
49
30
|
query: z.string().describe("Gmail search query"),
|
|
@@ -63,7 +44,7 @@ export function registerMessageTools(server, gmail) {
|
|
|
63
44
|
return { content: [{ type: "text", text }] };
|
|
64
45
|
}
|
|
65
46
|
catch (err) {
|
|
66
|
-
return wrapGmailError(err);
|
|
47
|
+
return wrapGmailError(err, "Message");
|
|
67
48
|
}
|
|
68
49
|
});
|
|
69
50
|
server.tool("gmail_get_message", "Get the full content of a Gmail message by ID", {
|
|
@@ -74,7 +55,7 @@ export function registerMessageTools(server, gmail) {
|
|
|
74
55
|
return { content: [{ type: "text", text: formatMessageDetail(msg) }] };
|
|
75
56
|
}
|
|
76
57
|
catch (err) {
|
|
77
|
-
return wrapGmailError(err);
|
|
58
|
+
return wrapGmailError(err, "Message");
|
|
78
59
|
}
|
|
79
60
|
});
|
|
80
61
|
server.tool("gmail_archive", "Archive a message (remove from inbox)", {
|
package/build/utils.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function extractHeaders(headers) {
|
|
2
|
+
const get = (name) => headers?.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ??
|
|
3
|
+
"";
|
|
4
|
+
return {
|
|
5
|
+
from: get("From"),
|
|
6
|
+
to: get("To"),
|
|
7
|
+
subject: get("Subject"),
|
|
8
|
+
date: get("Date"),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function decodeBase64Url(data) {
|
|
12
|
+
return Buffer.from(data, "base64url").toString("utf-8");
|
|
13
|
+
}
|
|
14
|
+
export function extractTextBody(payload) {
|
|
15
|
+
// Direct body
|
|
16
|
+
if (payload.mimeType === "text/plain" && payload.body?.data) {
|
|
17
|
+
return decodeBase64Url(payload.body.data);
|
|
18
|
+
}
|
|
19
|
+
// Multipart — recurse
|
|
20
|
+
if (payload.parts) {
|
|
21
|
+
// Prefer text/plain
|
|
22
|
+
for (const part of payload.parts) {
|
|
23
|
+
if (part.mimeType === "text/plain" && part.body?.data) {
|
|
24
|
+
return decodeBase64Url(part.body.data);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Fall back to text/html with tag stripping
|
|
28
|
+
for (const part of payload.parts) {
|
|
29
|
+
if (part.mimeType === "text/html" && part.body?.data) {
|
|
30
|
+
return stripHtml(decodeBase64Url(part.body.data));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Recurse into nested multipart
|
|
34
|
+
for (const part of payload.parts) {
|
|
35
|
+
const text = extractTextBody(part);
|
|
36
|
+
if (text)
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Fallback: HTML body at top level
|
|
41
|
+
if (payload.mimeType === "text/html" && payload.body?.data) {
|
|
42
|
+
return stripHtml(decodeBase64Url(payload.body.data));
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
export function stripHtml(html) {
|
|
47
|
+
return html
|
|
48
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
49
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
50
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
51
|
+
.replace(/<\/p>/gi, "\n\n")
|
|
52
|
+
.replace(/<[^>]+>/g, "")
|
|
53
|
+
.replace(/ /g, " ")
|
|
54
|
+
.replace(/&/g, "&")
|
|
55
|
+
.replace(/</g, "<")
|
|
56
|
+
.replace(/>/g, ">")
|
|
57
|
+
.replace(/"/g, '"')
|
|
58
|
+
.replace(/'/g, "'")
|
|
59
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-mato/gmail-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "MCP server for Gmail - search, read, archive, and manage your email",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,19 +25,21 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
28
|
-
"googleapis": "^
|
|
29
|
-
"open": "^
|
|
30
|
-
"zod": "^3.
|
|
28
|
+
"googleapis": "^171.4.0",
|
|
29
|
+
"open": "^11.0.0",
|
|
30
|
+
"zod": "^4.3.6"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@biomejs/biome": "^2.4.8",
|
|
34
|
-
"@types/node": "^
|
|
35
|
-
"typescript": "6.0.0-beta"
|
|
34
|
+
"@types/node": "^25.5.0",
|
|
35
|
+
"typescript": "6.0.0-beta",
|
|
36
|
+
"vitest": "^4.1.0"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsc",
|
|
39
40
|
"dev": "tsc --watch",
|
|
40
41
|
"typecheck": "tsc --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
41
43
|
"lint": "biome check .",
|
|
42
44
|
"lint:fix": "biome check --write ."
|
|
43
45
|
}
|