windsurfmcp-aws-mcp-server1
Version:
Claude Desktop用のAWS MCPサーバーパッケージです。WebSocketを通じてAWS上のLambda関数に接続し、ZohoのCRM/Desk/Books APIとの連携機能を提供します。
234 lines (211 loc) • 8.88 kB
JavaScript
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;