@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 +13 -15
- package/dist/index.js +24 -10
- package/package.json +16 -16
- package/dist/auth.js +0 -70
- package/dist/permissions.js +0 -94
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
|
-
|
|
53
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 "
|
|
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
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
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: "
|
|
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": "
|
|
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
|
-
"
|
|
13
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
40
|
-
"
|
|
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
|
-
}
|
package/dist/permissions.js
DELETED
|
@@ -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
|
-
}
|