@kazuph/mcp-gmail-gas

#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import type { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; interface ServerConfig { gas?: { endpoint: string; apiKey: string; }; } // 環境変数から設定を読み込む const config: ServerConfig = { gas: process.env.GAS_ENDPOINT && process.env.VALID_API_KEY ? { endpoint: process.env.GAS_ENDPOINT, apiKey: process.env.VALID_API_KEY, } : undefined, }; // 設定のバリデーション if (!config.gas?.endpoint || !config.gas?.apiKey) { console.error( "GAS configuration is missing. Please check your environment variables." ); process.exit(1); } // スキーマ定義 const GmailSearchSchema = z.object({ query: z.string().nonempty("query is required"), }); const GmailGetMessageSchema = z.object({ messageId: z.string().nonempty("messageId is required"), }); const GmailDownloadAttachmentSchema = z.object({ messageId: z.string().nonempty("messageId is required"), attachmentId: z.string().nonempty("attachmentId is required"), outputFilename: z.string().optional(), }); // MCPサーバーインスタンス作成 const server = new Server( { name: "mcp-gmail", version: "0.0.2", }, { capabilities: { tools: {}, }, } ); // ListToolsハンドラー server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = [ { name: "gmail_search_messages", description: ` Gmail内で指定したクエリに一致するメールを検索します。 queryパラメータはGmailの検索クエリ形式で指定します。 例: "subject:Meeting newer_than:1d" 結果はJSONで返り、メール一覧(件名、messageIdなど)を含みます。 `, inputSchema: zodToJsonSchema(GmailSearchSchema) as ReturnType< typeof zodToJsonSchema >, }, { name: "gmail_get_message", description: ` 指定したmessageIdのメール本文と詳細を取得します。 引数: messageId (GmailのメッセージID) `, inputSchema: zodToJsonSchema(GmailGetMessageSchema) as ReturnType< typeof zodToJsonSchema >, }, { name: "gmail_download_attachment", description: ` 指定したmessageIdとattachmentIdで添付ファイルを取得します。 ファイルはDownloadsフォルダに保存されます。 attachmentIdはattachmentsの各attachmentのnameでありファイル名となることが多いです(invoice.pdfなど)。 引数: - messageId: メッセージID(必須) - attachmentId: 添付ファイルID(必須) - outputFilename: 保存時のファイル名(オプション) `, inputSchema: zodToJsonSchema(GmailDownloadAttachmentSchema) as ReturnType< typeof zodToJsonSchema >, }, ]; return { tools }; }); // 共通のFetch関数 (GASエンドポイントへアクセス) async function callGAS( action: string, params: Record<string, string> ): Promise<any> { if (!config.gas) { throw new Error("GAS configuration is missing"); } const url = new URL(config.gas.endpoint); url.searchParams.set("action", action); url.searchParams.set("apiKey", config.gas.apiKey); for (const key of Object.keys(params)) { url.searchParams.set(key, params[key]); } const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } // Tool呼び出しハンドラー server.setRequestHandler( CallToolRequestSchema, async (request: CallToolRequest) => { try { const { name, arguments: args } = request.params; switch (name) { case "gmail_search_messages": { const parsed = GmailSearchSchema.safeParse(args); if (!parsed.success) { throw new Error( `Invalid arguments for gmail_search_messages: ${parsed.error}` ); } const data = await callGAS("search", { query: parsed.data.query }); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } case "gmail_get_message": { const parsed = GmailGetMessageSchema.safeParse(args); if (!parsed.success) { throw new Error( `Invalid arguments for gmail_get_message: ${parsed.error}` ); } const data = await callGAS("getMessage", { messageId: parsed.data.messageId, }); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } case "gmail_download_attachment": { const parsed = GmailDownloadAttachmentSchema.safeParse(args); if (!parsed.success) { throw new Error( `Invalid arguments for gmail_download_attachment: ${parsed.error}` ); } const data = await callGAS("downloadAttachment", { messageId: parsed.data.messageId, attachmentId: parsed.data.attachmentId, }); // 添付ファイルをDownloadsフォルダに保存 const attachment = data.attachment; if (!attachment || !attachment.base64 || !attachment.name) { throw new Error("Invalid attachment data from API"); } const downloadsDir = path.join(os.homedir(), "Downloads"); const filePath = path.join( downloadsDir, parsed.data.outputFilename || attachment.name ); const fileBuffer = Buffer.from(attachment.base64, "base64"); fs.writeFileSync(filePath, fileBuffer); return { content: [ { type: "text", text: `添付ファイルを ${filePath} に保存しました。`, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } } ); // サーバー起動 async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Gmail Server running on stdio with API Key auth"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });