@shivaduke28/gmail-mcp 0.2.0 → 1.1.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,35 +3,39 @@ 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, resolvePath } 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
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
- const credentialsPath = process.env.GOOGLE_OAUTH_CREDENTIALS;
13
- if (!credentialsPath) {
12
+ const SCOPES = [
13
+ "https://www.googleapis.com/auth/gmail.modify",
14
+ ];
15
+ const rawCredentialsPath = process.env.GOOGLE_OAUTH_CREDENTIALS;
16
+ if (!rawCredentialsPath) {
14
17
  console.error("GOOGLE_OAUTH_CREDENTIALS 環境変数を設定してください");
15
18
  process.exit(1);
16
19
  }
20
+ const credentialsPath = resolvePath(rawCredentialsPath);
17
21
  if (!existsSync(credentialsPath)) {
18
22
  console.error(`credentials.json が見つかりません: ${credentialsPath}`);
19
23
  process.exit(1);
20
24
  }
21
25
  const resolvedCredentialsPath = credentialsPath;
22
- const resolvedTokensPath = process.env.GOOGLE_OAUTH_TOKENS ?? join(homedir(), ".config", "gmail-mcp", "tokens.json");
26
+ const resolvedTokensPath = process.env.GOOGLE_OAUTH_TOKENS ? resolvePath(process.env.GOOGLE_OAUTH_TOKENS) : join(homedir(), ".config", "gmail-mcp", "tokens.json");
23
27
  // lazy auth: ツール呼び出し時に初めて認証する
24
28
  let gmailClient = null;
25
29
  async function getGmail() {
26
30
  if (!gmailClient) {
27
- const auth = await authorize(resolvedCredentialsPath, resolvedTokensPath);
31
+ const auth = await authorize(resolvedCredentialsPath, resolvedTokensPath, SCOPES);
28
32
  gmailClient = googleGmail({ version: "v1", auth });
29
33
  }
30
34
  return gmailClient;
31
35
  }
32
36
  const server = new McpServer({
33
37
  name: "gmail-mcp",
34
- version: "0.2.0",
38
+ version: "1.1.0",
35
39
  });
36
40
  // 1. search-messages
37
41
  server.registerTool("search-messages", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaduke28/gmail-mcp",
3
- "version": "0.2.0",
3
+ "version": "1.1.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.1.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,88 +0,0 @@
1
- import { OAuth2Client } from "google-auth-library";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
4
- import { createServer } from "node:http";
5
- import { exec } from "node:child_process";
6
- const SCOPES = [
7
- "https://www.googleapis.com/auth/gmail.modify",
8
- ];
9
- export async function authorize(credentialsPath, tokensPath) {
10
- const content = await readFile(credentialsPath, "utf-8");
11
- const credentials = JSON.parse(content);
12
- const { client_id, client_secret } = credentials.installed;
13
- const oauth2Client = new OAuth2Client(client_id, client_secret, "http://localhost:3000/callback");
14
- // 保存済みトークンがあれば読み込み、有効性を確認
15
- let hasTokens = false;
16
- try {
17
- const tokens = JSON.parse(await readFile(tokensPath, "utf-8"));
18
- oauth2Client.setCredentials(tokens);
19
- hasTokens = true;
20
- }
21
- catch {
22
- // ファイルなしまたは破損
23
- }
24
- if (hasTokens) {
25
- try {
26
- await oauth2Client.getAccessToken();
27
- return oauth2Client;
28
- }
29
- catch (err) {
30
- const status = err.status ?? err.code;
31
- if (status === 401 || status === 400) {
32
- // トークン失効 → ブラウザ認証へ
33
- }
34
- else {
35
- throw err;
36
- }
37
- }
38
- }
39
- const tokens = await authenticateWithBrowser(oauth2Client);
40
- await mkdir(dirname(tokensPath), { recursive: true });
41
- await writeFile(tokensPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
42
- oauth2Client.setCredentials(tokens);
43
- return oauth2Client;
44
- }
45
- function authenticateWithBrowser(oauth2Client) {
46
- return new Promise((resolve, reject) => {
47
- const authUrl = oauth2Client.generateAuthUrl({
48
- access_type: "offline",
49
- scope: SCOPES,
50
- });
51
- const server = createServer(async (req, res) => {
52
- if (!req.url?.startsWith("/callback"))
53
- return;
54
- const url = new URL(req.url, "http://localhost:3000");
55
- const code = url.searchParams.get("code");
56
- if (!code) {
57
- res.writeHead(400);
58
- res.end("No code received");
59
- reject(new Error("No authorization code received"));
60
- server.close();
61
- return;
62
- }
63
- try {
64
- const { tokens } = await oauth2Client.getToken(code);
65
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
66
- res.end("<h1>認証成功!このタブを閉じてください。</h1>");
67
- resolve(tokens);
68
- }
69
- catch (err) {
70
- res.writeHead(500);
71
- res.end("Token exchange failed");
72
- reject(err);
73
- }
74
- finally {
75
- server.close();
76
- }
77
- });
78
- server.listen(3000, () => {
79
- console.error(`\n認証が必要です。ブラウザを開きます...\n`);
80
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
81
- exec(`${command} '${authUrl}'`, (err) => {
82
- if (err) {
83
- console.error(`ブラウザの自動起動に失敗しました。以下のURLを手動で開いてください:\n${authUrl}\n`);
84
- }
85
- });
86
- });
87
- });
88
- }
@@ -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
- }