UNPKG

windsurfmcp-aws-mcp-server1

Version:

Claude Desktop用のAWS MCPサーバーパッケージです。WebSocketを通じてAWS上のLambda関数に接続し、ZohoのCRM/Desk/Books APIとの連携機能を提供します。

234 lines (211 loc) 8.88 kB
import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import crypto from "crypto"; import { logger } from "./logger.mjs"; import os from "os"; import { zohoConfig } from "../config/zoho-config.mjs"; // __dirnameの代替(ESモジュールでは使用できないため) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // トークンの保存先ディレクトリ const TOKEN_DIR = path.join(os.homedir(), "AppData", "Roaming", "ZohoMCP", "tokens"); // 暗号化キー(環境変数から取得、なければデフォルト値を使用) const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || "zoho-mcp-integration-default-key-2024"; const IV_LENGTH = 16; // キーのハッシュ化(32バイトの固定長キーを生成) const KEY_HASH = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest(); // トークンマネージャークラス class TokenManager { constructor() { // トークンディレクトリの作成 if (!fs.existsSync(TOKEN_DIR)) { fs.mkdirSync(TOKEN_DIR, { recursive: true }); } } // トークンファイルのパスを取得 getTokenPath(service) { return path.join(TOKEN_DIR, `${service}_token.json`); } // トークンの暗号化 encrypt(text) { try { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv("aes-256-cbc", KEY_HASH, iv); let encrypted = cipher.update(text, "utf8", "hex"); encrypted += cipher.final("hex"); return iv.toString("hex") + ":" + encrypted; } catch (error) { logger.error(`暗号化エラー: ${error.message}`); throw error; } } // トークンの復号化 decrypt(text) { try { const textParts = text.split(":"); if (textParts.length !== 2) { throw new Error("無効な暗号化テキスト形式"); } const iv = Buffer.from(textParts[0], "hex"); const encryptedText = Buffer.from(textParts[1], "hex"); const decipher = crypto.createDecipheriv("aes-256-cbc", KEY_HASH, iv); let decrypted = decipher.update(encryptedText, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } catch (error) { logger.error(`復号化エラー: ${error.message}`); throw error; } } // トークンの保存 async saveTokens(service, tokens) { try { const tokenPath = this.getTokenPath(service); // タイムスタンプを追加 const tokenData = { ...tokens, timestamp: Date.now() }; const encryptedTokens = this.encrypt(JSON.stringify(tokenData)); fs.writeFileSync(tokenPath, encryptedTokens, { mode: 0o600 }); // 所有者のみ読み書き可能 logger.info(`${service}のトークンを保存しました`); return true; } catch (error) { logger.error(`${service}のトークン保存中にエラーが発生しました: ${error.message}`); return false; } } // トークンの読み込み async loadTokens(service) { try { const tokenPath = this.getTokenPath(service); if (!fs.existsSync(tokenPath)) { logger.warn(`${service}のトークンファイルが存在しません`); return null; } const encryptedTokens = fs.readFileSync(tokenPath, "utf8"); const decryptedTokens = this.decrypt(encryptedTokens); return JSON.parse(decryptedTokens); } catch (error) { logger.error(`${service}のトークン読み込み中にエラーが発生しました: ${error.message}`); return null; } } // トークンの有効性確認 async hasValidToken(service) { try { const tokens = await this.loadTokens(service); if (!tokens) { return false; } const now = Date.now(); const expiresAt = tokens.timestamp + (tokens.expires_in * 1000); return now < expiresAt; } catch (error) { logger.error(`${service}のトークン検証中にエラーが発生しました: ${error.message}`); return false; } } // 有効なアクセストークンの取得 async getValidAccessToken(service) { try { const tokens = await this.loadTokens(service); if (!tokens) { throw new Error(`${service}のトークンが見つかりません`); } const now = Date.now(); const expiresAt = tokens.timestamp + (tokens.expires_in * 1000); if (now >= expiresAt) { throw new Error(`${service}のトークンが期限切れです`); } return tokens.access_token; } catch (error) { logger.error(`${service}のアクセストークン取得中にエラーが発生しました: ${error.message}`); throw error; } } // すべてのトークンの削除 async deleteAllTokens() { try { const files = fs.readdirSync(TOKEN_DIR); for (const file of files) { if (file.endsWith("_token.json")) { fs.unlinkSync(path.join(TOKEN_DIR, file)); logger.info(`トークンファイルを削除しました: ${file}`); } } return true; } catch (error) { logger.error(`トークンの削除中にエラーが発生しました: ${error.message}`); return false; } } // リフレッシュトークンの存在確認 async hasRefreshToken(service) { try { const tokens = await this.loadTokens(service); return tokens && tokens.refresh_token ? true : false; } catch (error) { logger.error(`${service}のリフレッシュトークン確認中にエラーが発生しました: ${error.message}`); return false; } } // トークンの取得(非同期メソッドを同期的に呼び出す) getTokens(service) { try { // loadTokensは非同期メソッドなので、同期的に実行 const tokens = fs.existsSync(this.getTokenPath(service)) ? JSON.parse(this.decrypt(fs.readFileSync(this.getTokenPath(service), "utf8"))) : null; return tokens; } catch (error) { logger.error(`${service}のトークン取得中にエラーが発生しました: ${error.message}`); return null; } } // APIドメインの取得 getApiDomainForService(service) { try { // zohoConfigからAPIベースURLを取得 const apiBaseUrl = zohoConfig.getApiBaseUrl(service); logger.info(`${service}のAPIドメインを取得: ${apiBaseUrl}`); return apiBaseUrl; } catch (error) { logger.error(`${service}のAPIドメイン取得中にエラーが発生しました`, error); // デフォルト値を返す(日本のデータセンター用) switch (service) { case "crm": return "https://www.zohoapis.com/crm/v3"; case "desk": return "https://desk.zoho.com/api/v1"; case "books": return "https://books.zoho.com/api/v3"; default: return ""; } } } // モックトークンの生成 async createMockTokens(service) { try { const mockTokens = { access_token: `mock_access_token_${service}_${Date.now()}`, refresh_token: `mock_refresh_token_${service}_${Date.now()}`, expires_in: 3600, api_domain: this.getApiDomainForService(service), timestamp: Date.now() }; await this.saveTokens(service, mockTokens); logger.info(`${service}のモックトークンを生成しました`); return mockTokens; } catch (error) { logger.error(`${service}のモックトークン生成中にエラーが発生しました: ${error.message}`); throw error; } } } // シングルトンインスタンスを作成 const tokenManager = new TokenManager(); export { tokenManager }; export default tokenManager;