@shivaduke28/gmail-mcp 0.1.0 → 1.0.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # gmail-mcp
1
+ # @shivaduke28/gmail-mcp
2
2
 
3
3
  Gmail API の MCP (Model Context Protocol) サーバー。
4
4
 
@@ -38,8 +38,7 @@ OAuth スコープは `gmail.modify` のみ。送信・削除はできません
38
38
  "command": "npx",
39
39
  "args": ["-y", "@shivaduke28/gmail-mcp"],
40
40
  "env": {
41
- "GOOGLE_OAUTH_CREDENTIALS": "/path/to/credentials.json",
42
- "GOOGLE_OAUTH_TOKENS": "/path/to/tokens.json"
41
+ "GOOGLE_OAUTH_CREDENTIALS": "/path/to/credentials.json"
43
42
  }
44
43
  }
45
44
  }
@@ -49,8 +48,8 @@ OAuth スコープは `gmail.modify` のみ。送信・削除はできません
49
48
  #### ソースから実行
50
49
 
51
50
  ```bash
52
- npm install
53
- npm run build
51
+ pnpm install
52
+ pnpm -r build
54
53
  ```
55
54
 
56
55
  ```json
@@ -58,10 +57,9 @@ npm run build
58
57
  "mcpServers": {
59
58
  "gmail": {
60
59
  "command": "node",
61
- "args": ["/path/to/gmail-mcp/dist/index.js"],
60
+ "args": ["/path/to/google-mcp/packages/gmail/dist/index.js"],
62
61
  "env": {
63
- "GOOGLE_OAUTH_CREDENTIALS": "/path/to/credentials.json",
64
- "GOOGLE_OAUTH_TOKENS": "/path/to/tokens.json"
62
+ "GOOGLE_OAUTH_CREDENTIALS": "/path/to/credentials.json"
65
63
  }
66
64
  }
67
65
  }
@@ -73,11 +71,12 @@ npm run build
73
71
  | 変数 | 必須 | 説明 |
74
72
  |---|---|---|
75
73
  | `GOOGLE_OAUTH_CREDENTIALS` | Yes | OAuth クライアント認証情報の JSON ファイルパス |
76
- | `GOOGLE_OAUTH_TOKENS` | Yes | ユーザートークンの保存先パス。初回認証時に自動生成 |
77
74
 
78
75
  ### 4. 認証
79
76
 
80
- 初回起動時にブラウザが開き、Google アカウントでの認証を求められます。認証後、トークンが `GOOGLE_OAUTH_TOKENS` で指定したパスに自動保存され、以降はブラウザ認証なしで起動できます。
77
+ 初回起動時にブラウザが開き、Google アカウントでの認証を求められます。認証後、トークンは `~/.config/gmail-mcp/tokens.json` に自動保存され、以降はブラウザ認証なしで起動できます。
78
+
79
+ PKCE (Proof Key for Code Exchange) に対応しています。
81
80
 
82
81
  ## Gmail 検索クエリの例
83
82
 
@@ -97,11 +96,10 @@ label:INBOX
97
96
  ## Development
98
97
 
99
98
  ```bash
100
- npm install
101
- npm run dev # tsx で開発実行
102
- npm run build # tsc でビルド
103
- npm run test # テスト実行
104
- npm run typecheck # 型チェック
99
+ pnpm install
100
+ pnpm --filter @shivaduke28/gmail-mcp dev # tsx で開発実行
101
+ pnpm --filter @shivaduke28/gmail-mcp build # tsc でビルド
102
+ pnpm --filter @shivaduke28/gmail-mcp typecheck # 型チェック
105
103
  ```
106
104
 
107
105
  ## License
package/dist/index.js CHANGED
@@ -3,30 +3,38 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { encode } from "@toon-format/toon";
6
- import { authorize } from "./auth.js";
6
+ import { authorize } from "@shivaduke28/google-mcp-auth";
7
7
  import { gmail as googleGmail } from "@googleapis/gmail";
8
8
  import { extractHeaders, extractBody, buildRawMessage } from "./gmail.js";
9
9
  import { existsSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ const SCOPES = [
13
+ "https://www.googleapis.com/auth/gmail.modify",
14
+ ];
10
15
  const credentialsPath = process.env.GOOGLE_OAUTH_CREDENTIALS;
11
- const tokensPath = process.env.GOOGLE_OAUTH_TOKENS;
12
16
  if (!credentialsPath) {
13
17
  console.error("GOOGLE_OAUTH_CREDENTIALS 環境変数を設定してください");
14
18
  process.exit(1);
15
19
  }
16
- if (!tokensPath) {
17
- console.error("GOOGLE_OAUTH_TOKENS 環境変数を設定してください");
18
- process.exit(1);
19
- }
20
20
  if (!existsSync(credentialsPath)) {
21
21
  console.error(`credentials.json が見つかりません: ${credentialsPath}`);
22
22
  process.exit(1);
23
23
  }
24
- // OAuth2認証
25
- const auth = await authorize(credentialsPath, tokensPath);
26
- const gmail = googleGmail({ version: "v1", auth });
24
+ const resolvedCredentialsPath = credentialsPath;
25
+ const resolvedTokensPath = process.env.GOOGLE_OAUTH_TOKENS ?? join(homedir(), ".config", "gmail-mcp", "tokens.json");
26
+ // lazy auth: ツール呼び出し時に初めて認証する
27
+ let gmailClient = null;
28
+ async function getGmail() {
29
+ if (!gmailClient) {
30
+ const auth = await authorize(resolvedCredentialsPath, resolvedTokensPath, SCOPES);
31
+ gmailClient = googleGmail({ version: "v1", auth });
32
+ }
33
+ return gmailClient;
34
+ }
27
35
  const server = new McpServer({
28
36
  name: "gmail-mcp",
29
- version: "0.1.0",
37
+ version: "1.0.0",
30
38
  });
31
39
  // 1. search-messages
32
40
  server.registerTool("search-messages", {
@@ -36,6 +44,7 @@ server.registerTool("search-messages", {
36
44
  maxResults: z.number().optional().default(20).describe("最大取得件数"),
37
45
  },
38
46
  }, async ({ query, maxResults }) => {
47
+ const gmail = await getGmail();
39
48
  const res = await gmail.users.messages.list({
40
49
  userId: "me",
41
50
  q: query,
@@ -83,6 +92,7 @@ server.registerTool("get-messages", {
83
92
  messageIds: z.array(z.string()).describe("メッセージIDの配列"),
84
93
  },
85
94
  }, async ({ messageIds }) => {
95
+ const gmail = await getGmail();
86
96
  const details = await Promise.all(messageIds.map((id) => gmail.users.messages.get({
87
97
  userId: "me",
88
98
  id,
@@ -117,6 +127,7 @@ server.registerTool("get-threads", {
117
127
  threadIds: z.array(z.string()).describe("スレッドIDの配列"),
118
128
  },
119
129
  }, async ({ threadIds }) => {
130
+ const gmail = await getGmail();
120
131
  const threads = await Promise.all(threadIds.map((id) => gmail.users.threads.get({
121
132
  userId: "me",
122
133
  id,
@@ -161,6 +172,7 @@ server.registerTool("create-draft", {
161
172
  inReplyToMessageId: z.string().optional().describe("返信先メッセージID(返信時に指定。Referencesヘッダー構築用)"),
162
173
  },
163
174
  }, async ({ to, cc, subject, body, threadId, inReplyToMessageId }) => {
175
+ const gmail = await getGmail();
164
176
  // 返信時のヘッダー構築
165
177
  let inReplyTo;
166
178
  let references;
@@ -210,6 +222,7 @@ server.registerTool("modify-labels", {
210
222
  removeLabelIds: z.array(z.string()).optional().default([]).describe("削除するラベルIDの配列"),
211
223
  },
212
224
  }, async ({ messageIds, addLabelIds, removeLabelIds }) => {
225
+ const gmail = await getGmail();
213
226
  await gmail.users.messages.batchModify({
214
227
  userId: "me",
215
228
  requestBody: {
@@ -230,6 +243,7 @@ server.registerTool("list-labels", {
230
243
  description: "利用可能なラベル一覧を取得する。レスポンスはTOON形式で返す。",
231
244
  inputSchema: {},
232
245
  }, async () => {
246
+ const gmail = await getGmail();
233
247
  const res = await gmail.users.labels.list({ userId: "me" });
234
248
  const labels = (res.data.labels ?? []).map((label) => ({
235
249
  id: label.id ?? "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaduke28/gmail-mcp",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -9,16 +9,13 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
- "scripts": {
13
- "build": "tsc",
14
- "start": "node dist/index.js",
15
- "dev": "tsx src/index.ts",
16
- "test": "tsx --test test/*.test.ts",
17
- "typecheck": "tsc --noEmit"
12
+ "publishConfig": {
13
+ "access": "public"
18
14
  },
19
15
  "repository": {
20
16
  "type": "git",
21
- "url": "git+https://github.com/shivaduke28/gmail-mcp.git"
17
+ "url": "https://github.com/shivaduke28/google-mcp.git",
18
+ "directory": "packages/gmail"
22
19
  },
23
20
  "keywords": [
24
21
  "mcp",
@@ -27,21 +24,24 @@
27
24
  ],
28
25
  "author": "shivaduke",
29
26
  "license": "ISC",
30
- "bugs": {
31
- "url": "https://github.com/shivaduke28/gmail-mcp/issues"
32
- },
33
- "homepage": "https://github.com/shivaduke28/gmail-mcp#readme",
34
- "description": "Gmail MCP server with domain-based permission control",
27
+ "description": "Gmail MCP server",
35
28
  "dependencies": {
36
29
  "@googleapis/gmail": "^16.1.1",
37
30
  "@modelcontextprotocol/sdk": "^1.26.0",
38
31
  "@toon-format/toon": "^2.1.0",
39
- "google-auth-library": "^10.5.0",
40
- "zod": "^4.3.6"
32
+ "zod": "^4.3.6",
33
+ "@shivaduke28/google-mcp-auth": "1.0.0"
41
34
  },
42
35
  "devDependencies": {
43
36
  "@types/node": "^25.2.3",
44
37
  "tsx": "^4.21.0",
45
38
  "typescript": "^5.9.3"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "start": "node dist/index.js",
43
+ "dev": "tsx src/index.ts",
44
+ "test": "tsx --test test/*.test.ts",
45
+ "typecheck": "tsc --noEmit"
46
46
  }
47
- }
47
+ }
package/dist/auth.js DELETED
@@ -1,70 +0,0 @@
1
- import { OAuth2Client } from "google-auth-library";
2
- import { readFile, writeFile } from "node:fs/promises";
3
- import { createServer } from "node:http";
4
- import { exec } from "node:child_process";
5
- const SCOPES = [
6
- "https://www.googleapis.com/auth/gmail.modify",
7
- ];
8
- export async function authorize(credentialsPath, tokensPath) {
9
- const content = await readFile(credentialsPath, "utf-8");
10
- const credentials = JSON.parse(content);
11
- const { client_id, client_secret } = credentials.installed;
12
- const oauth2Client = new OAuth2Client(client_id, client_secret, "http://localhost:3000/callback");
13
- // 保存済みトークンがあれば読み込む
14
- try {
15
- const tokens = JSON.parse(await readFile(tokensPath, "utf-8"));
16
- oauth2Client.setCredentials(tokens);
17
- return oauth2Client;
18
- }
19
- catch {
20
- // トークンがなければブラウザ認証
21
- }
22
- const tokens = await authenticateWithBrowser(oauth2Client);
23
- await writeFile(tokensPath, JSON.stringify(tokens, null, 2));
24
- oauth2Client.setCredentials(tokens);
25
- return oauth2Client;
26
- }
27
- function authenticateWithBrowser(oauth2Client) {
28
- return new Promise((resolve, reject) => {
29
- const authUrl = oauth2Client.generateAuthUrl({
30
- access_type: "offline",
31
- scope: SCOPES,
32
- });
33
- const server = createServer(async (req, res) => {
34
- if (!req.url?.startsWith("/callback"))
35
- return;
36
- const url = new URL(req.url, "http://localhost:3000");
37
- const code = url.searchParams.get("code");
38
- if (!code) {
39
- res.writeHead(400);
40
- res.end("No code received");
41
- reject(new Error("No authorization code received"));
42
- server.close();
43
- return;
44
- }
45
- try {
46
- const { tokens } = await oauth2Client.getToken(code);
47
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
48
- res.end("<h1>認証成功!このタブを閉じてください。</h1>");
49
- resolve(tokens);
50
- }
51
- catch (err) {
52
- res.writeHead(500);
53
- res.end("Token exchange failed");
54
- reject(err);
55
- }
56
- finally {
57
- server.close();
58
- }
59
- });
60
- server.listen(3000, () => {
61
- console.error(`\n認証が必要です。ブラウザを開きます...\n`);
62
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
63
- exec(`${command} '${authUrl}'`, (err) => {
64
- if (err) {
65
- console.error(`ブラウザの自動起動に失敗しました。以下のURLを手動で開いてください:\n${authUrl}\n`);
66
- }
67
- });
68
- });
69
- });
70
- }
@@ -1,94 +0,0 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { existsSync } from "node:fs";
3
- export const PermissionAction = {
4
- Allow: "allow",
5
- Deny: "deny",
6
- };
7
- export const OperationType = {
8
- Read: "read",
9
- CreateDraft: "create_draft",
10
- ModifyLabels: "modify_labels",
11
- };
12
- const DEFAULT_CONFIG = {
13
- internalDomain: "",
14
- permissions: {
15
- read: {
16
- self_only: PermissionAction.Allow,
17
- internal: PermissionAction.Allow,
18
- external: PermissionAction.Allow,
19
- },
20
- create_draft: {
21
- self_only: PermissionAction.Allow,
22
- internal: PermissionAction.Allow,
23
- external: PermissionAction.Deny,
24
- },
25
- modify_labels: {
26
- self_only: PermissionAction.Allow,
27
- internal: PermissionAction.Allow,
28
- external: PermissionAction.Allow,
29
- },
30
- },
31
- };
32
- export async function loadPermissionConfig(configPath) {
33
- if (!configPath)
34
- return DEFAULT_CONFIG;
35
- // ファイルがなければデフォルト設定を書き出す
36
- if (!existsSync(configPath)) {
37
- try {
38
- await writeFile(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
39
- console.error(`permissions.json を作成しました: ${configPath}`);
40
- }
41
- catch {
42
- console.error(`permissions.json の作成に失敗しました: ${configPath}`);
43
- }
44
- return DEFAULT_CONFIG;
45
- }
46
- try {
47
- const content = await readFile(configPath, "utf-8");
48
- const parsed = JSON.parse(content);
49
- return {
50
- internalDomain: parsed.internalDomain ?? DEFAULT_CONFIG.internalDomain,
51
- permissions: {
52
- read: parsed.permissions?.read ?? DEFAULT_CONFIG.permissions.read,
53
- create_draft: parsed.permissions?.create_draft ?? DEFAULT_CONFIG.permissions.create_draft,
54
- modify_labels: parsed.permissions?.modify_labels ?? DEFAULT_CONFIG.permissions.modify_labels,
55
- },
56
- };
57
- }
58
- catch {
59
- console.error(`設定ファイルの読み込みに失敗しました: ${configPath}`);
60
- return DEFAULT_CONFIG;
61
- }
62
- }
63
- export const RecipientCondition = {
64
- SelfOnly: "self_only",
65
- Internal: "internal",
66
- External: "external",
67
- };
68
- export function classifyRecipients(recipients, selfEmail, internalDomain) {
69
- const others = recipients.filter((email) => email.toLowerCase() !== selfEmail.toLowerCase());
70
- if (others.length === 0)
71
- return RecipientCondition.SelfOnly;
72
- if (internalDomain && others.every((email) => email.toLowerCase().endsWith(`@${internalDomain.toLowerCase()}`))) {
73
- return RecipientCondition.Internal;
74
- }
75
- return RecipientCondition.External;
76
- }
77
- export function checkPermission(config, operation, recipients, selfEmail) {
78
- const perm = config.permissions[operation];
79
- const condition = classifyRecipients(recipients, selfEmail, config.internalDomain);
80
- return { action: perm[condition], condition };
81
- }
82
- const CONDITION_LABELS = {
83
- [RecipientCondition.SelfOnly]: "自分のみ",
84
- [RecipientCondition.Internal]: "内部メンバー",
85
- [RecipientCondition.External]: "外部宛先",
86
- };
87
- const OPERATION_LABELS = {
88
- [OperationType.Read]: "読み取り",
89
- [OperationType.CreateDraft]: "下書き作成",
90
- [OperationType.ModifyLabels]: "ラベル変更",
91
- };
92
- export function denyMessage(operation, condition) {
93
- return `${CONDITION_LABELS[condition]}への${OPERATION_LABELS[operation]}は許可されていません。`;
94
- }