@d-mato/gmail-mcp 0.1.1 → 0.1.3

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,65 +1,5 @@
1
1
  import { google } from "googleapis";
2
- function extractHeaders(headers) {
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(/&nbsp;/g, " ")
55
- .replace(/&amp;/g, "&")
56
- .replace(/&lt;/g, "<")
57
- .replace(/&gt;/g, ">")
58
- .replace(/&quot;/g, '"')
59
- .replace(/&#39;/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
+ }
@@ -1,24 +1,5 @@
1
1
  import { z } from "zod";
2
- function errorResponse(message) {
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", {
@@ -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(/&nbsp;/g, " ")
54
+ .replace(/&amp;/g, "&")
55
+ .replace(/&lt;/g, "<")
56
+ .replace(/&gt;/g, ">")
57
+ .replace(/&quot;/g, '"')
58
+ .replace(/&#39;/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.1",
3
+ "version": "0.1.3",
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": "^148.0.0",
28
+ "googleapis": "^171.4.0",
29
29
  "open": "^10.0.0",
30
30
  "zod": "^3.24.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@biomejs/biome": "^2.4.8",
34
- "@types/node": "^22.0.0",
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
  }