UNPKG

windsurfmcp-aws-mcp-server1

Version:

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

581 lines (525 loc) 24.8 kB
import { logger } from "../utils/logger.mjs"; import { zohoConfig } from "../config/zoho-config.mjs"; import { tokenManager } from "../utils/token-manager.mjs"; /** * Zoho Desk APIクライアントクラス */ class ZohoDeskClient { constructor() { this.service = "desk"; // 設定から組織IDを取得 this.organizationId = zohoConfig.getServiceConfig(this.service).organizationId; if (!this.organizationId) { logger.warn("Zoho Desk組織IDが設定されていません"); } else { logger.info(`Zoho Desk組織ID: ${this.organizationId}`); } } /** * 接続状態を確認 * @returns {Promise<boolean>} 接続状態 */ async checkConnection() { try { logger.info("Zoho Desk接続状態を確認します"); // アクセストークンを取得(自動更新機能付き) const accessToken = await tokenManager.getAccessToken(this.service); if (!accessToken) { logger.warn("Zoho Deskの有効なアクセストークンがありません"); return false; } // 組織情報を取得して接続テスト const baseUrl = zohoConfig.getApiBaseUrl(this.service); const url = `${baseUrl}/organizations`; const response = await fetch(url, { headers: { "Authorization": `Zoho-oauthtoken ${accessToken}`, "Content-Type": "application/json" } }); const data = await response.json(); if (response.ok && data && data.data) { logger.info("Zoho Deskに正常に接続されています"); return true; } else { logger.warn("Zoho Deskから組織情報を取得できませんでした"); return false; } } catch (error) { logger.error("Zoho Desk接続確認中にエラーが発生しました", error); return false; } } /** * APIリクエストを実行 * @param {string} endpoint - APIエンドポイント * @param {object} options - リクエストオプション * @returns {Promise<object>} レスポンスデータ */ async makeRequest(endpoint, options = {}) { try { // アクセストークンを取得(自動更新機能付き) const accessToken = await tokenManager.getValidAccessToken(this.service); if (!accessToken) { throw new Error("有効なアクセストークンがありません"); } // APIドメインは設定ファイルから取得(Deskは固定) const apiBaseUrl = zohoConfig.getApiBaseUrl(this.service); logger.info(`Zoho Desk APIベースURL: ${apiBaseUrl}`); // 組織IDを取得 const organizationId = String(options.organizationId || this.organizationId); if (!organizationId) { throw new Error("組織IDが指定されていません"); } logger.info(`使用する組織ID: ${organizationId}`); // デフォルトのオプション const defaultOptions = { method: "GET", headers: { "Authorization": `Zoho-oauthtoken ${accessToken}`, "orgId": organizationId, "Content-Type": "application/json" } }; // オプションをマージ const requestOptions = { ...defaultOptions, ...options, headers: { ...defaultOptions.headers, ...(options.headers || {}) } }; // リクエストURLを構築 const requestUrl = apiBaseUrl.replace(/\/$/, "") + "/" + endpoint.replace(/^\//, ""); logger.info(`${this.service}リクエスト: ${requestOptions.method} ${requestUrl}`); // リクエストを実行 logger.info(`${this.service}リクエストを実行します...`); const response = await fetch(requestUrl, requestOptions); logger.info(`${this.service}レスポンスステータス: ${response.status} ${response.statusText}`); // レスポンスの処理 let data; try { const responseClone = response.clone(); // レスポンスのテキストを取得して確認 const textResponse = await responseClone.text(); logger.info(`レスポンステキスト(先頭100文字): ${textResponse.substring(0, 100)}...`); // HTMLレスポンスの場合はエラーとして処理 if (textResponse.trim().startsWith("<html")) { logger.error("APIがHTMLレスポンスを返しました。APIエンドポイントが正しくない可能性があります。"); logger.error(`使用したURL: ${requestUrl}`); return { error: "API endpoint is invalid or authentication failed (HTML response).", status: response.status, data: [] }; } // JSONとして解析 data = JSON.parse(textResponse); logger.info(`${this.service}レスポンスデータ: ${JSON.stringify(data).substring(0, 200)}...`); } catch (error) { logger.error(`JSONパースエラー: ${error.message}`); return { error: "JSON parse error", status: response.status, data: [] }; } // 401エラー時はトークン自動リフレッシュ→再試行 if (response.status === 401) { logger.warn("認証エラー: 401 Unauthorized。トークンをリフレッシュして再試行します..."); const refreshed = await tokenManager.refreshToken(this.service); if (refreshed) { logger.info("トークンリフレッシュ成功。再試行します..."); return await this.makeRequest(endpoint, options); } else { logger.error("トークンリフレッシュ失敗。再認証が必要です"); return { error: "Authentication failed. Please re-authenticate.", status: 401, data: [] }; } } // 422 Unprocessable Entity などAPIエラー時はエラー内容を返す if (!response.ok) { const errorMessage = data.message || data.errorCode || response.statusText; logger.error(`${this.service}APIエラー: ${errorMessage}`, data); return { error: errorMessage, status: response.status, data: [] }; } return data; } catch (error) { logger.error(`${this.service}リクエスト中にエラーが発生しました: ${error.message}`, error); return { error: error.message, status: 500, data: [] }; } } /** * モジュールからデータを取得 * @param {string} module - モジュール名(tickets, contacts等) * @param {string} organizationId - 組織ID * @param {object} filters - 検索フィルター * @returns {Promise<object>} データ一覧 */ async getData(module, organizationId, filters = {}) { try { logger.info(`Zoho Desk ${module}データを取得します`); // 組織IDを文字列に変換 const orgId = String(organizationId); logger.info(`使用する組織ID: ${orgId}`); // フィルター条件のログ出力 logger.info("フィルター条件:"); Object.keys(filters).forEach(key => { logger.info(` - ${key}: ${filters[key]}`); }); // エンドポイントを構築 let endpoint = ""; // モジュールに応じてエンドポイントを設定 if (module === "tickets" && filters.criteria) { // 検索APIを使用 endpoint = "/tickets/search"; } else if (module === "tickets") { endpoint = "/tickets"; } else if (module === "contacts") { endpoint = "/contacts"; } else if (module === "departments") { endpoint = "/departments"; } else if (module === "agents") { endpoint = "/agents"; } else if (module === "teams") { endpoint = "/teams"; } else { endpoint = `/${module}`; } // /organizations モジュールはクエリパラメータ不要 if (module !== "organizations") { const queryParams = new URLSearchParams(); if (module === "tickets" && filters.criteria) { // 検索API向けパラメータ queryParams.append("criteria", filters.criteria); queryParams.append("limit", filters.limit || "100"); queryParams.append("from", filters.from || "1"); } else { // デフォルトパラメータ queryParams.append("limit", filters.limit || "100"); queryParams.append("from", filters.from || "1"); // その他のフィルター const excluded = ["limit", "from"]; Object.keys(filters).forEach(key => { if (!excluded.includes(key)) { if (typeof filters[key] === "object" && filters[key] !== null) { Object.entries(filters[key]).forEach(([subKey, subVal]) => { queryParams.append(`${key}[${subKey}]`, subVal); }); } else { queryParams.append(key, filters[key]); } } }); } endpoint += `?${queryParams.toString()}`; } logger.debug("クエリパラメータ:"); logger.debug(`最終的なエンドポイント: ${endpoint}`); // リクエストを実行 const response = await this.makeRequest(endpoint, { organizationId: orgId }); // レスポンスからデータを抽出して整形 const result = { data: response.data || [], info: { count: response.info ? response.info.count : 0, from: response.info ? response.info.from : 1, limit: response.info ? response.info.limit : 100, hasMore: response.info ? response.info.hasMore : false } }; // レスポンスのログ出力 if (result.data.length > 0) { logger.info(`${module}から${result.data.length}件のデータを取得しました`); logger.info(`ページ情報: ${result.info.from}から${result.info.limit}件 (全${result.info.count}件)`); logger.info(`次のページがあるか: ${result.info.hasMore ? "はい" : "いいえ"}`); } else { logger.info(`${module}からデータを取得できませんでした`); } return result; } catch (error) { logger.error(`${module}のデータ取得に失敗しました: ${error.message}`, error); // エラー時には空のデータ構造を返す return { data: [], info: { count: 0, from: 1, limit: 100, hasMore: false } }; } } /** * チケット一覧を取得 * @param {object} params - クエリパラメータ * @returns {Promise<object>} チケット一覧 */ async getTickets(params = {}) { return this.getData("tickets", this.organizationId, params); } /** * 特定のチケットを取得 * @param {string} ticketId - チケットID * @returns {Promise<object>} チケット詳細 */ async getTicket(ticketId) { try { logger.info(`チケット(ID: ${ticketId})を取得します`); const response = await this.makeRequest(`tickets/${ticketId}`); return response; } catch (error) { logger.error(`チケット(ID: ${ticketId})の取得に失敗しました: ${error.message}`); return { data: null }; } } /** * チケットを作成 * @param {object} data - チケットデータ * @returns {Promise<object>} 作成されたチケット */ async createTicket(data) { try { logger.info("新しいチケットを作成します"); // API仕様外フィールドを除外 const allowedFields = ["subject","departmentId","contactId","email","description","priority","status"]; const filtered = {}; for (const k of allowedFields) { if (data[k] !== undefined) filtered[k] = data[k]; } const response = await this.makeRequest("tickets", { method: "POST", body: JSON.stringify(filtered) }); return response; } catch (error) { logger.error(`チケットの作成に失敗しました: ${error.message}`); return { error: error.message, status: 500, data: null }; } } /** * チケットを更新 * @param {string} ticketId - チケットID * @param {object} data - 更新データ * @returns {Promise<object>} 更新されたチケット */ async updateTicket(ticketId, data) { try { logger.info(`チケット(ID: ${ticketId})を更新します`); const response = await this.makeRequest(`tickets/${ticketId}`, { method: "PATCH", body: JSON.stringify(data) }); return response; } catch (error) { logger.error(`チケット(ID: ${ticketId})の更新に失敗しました: ${error.message}`); return { data: null }; } } /** * チケットにコメントを追加 * @param {string} ticketId - チケットID * @param {object} data - コメントデータ * @returns {Promise<object>} 作成されたコメント */ async addComment(ticketId, data) { try { logger.info(`チケット(ID: ${ticketId})にコメントを追加します`); const response = await this.makeRequest(`tickets/${ticketId}/comments`, { method: "POST", body: JSON.stringify(data) }); return response; } catch (error) { logger.error(`チケット(ID: ${ticketId})へのコメント追加に失敗しました: ${error.message}`); return { data: null }; } } /** * チケットのコメント一覧を取得 * @param {string} ticketId - チケットID * @returns {Promise<object>} コメント一覧 */ async getComments(ticketId) { try { logger.info(`チケット(ID: ${ticketId})のコメント一覧を取得します`); const response = await this.makeRequest(`tickets/${ticketId}/comments`); return response; } catch (error) { logger.error(`チケット(ID: ${ticketId})のコメント一覧の取得に失敗しました: ${error.message}`); return { data: null }; } } /** * チケットの添付ファイル一覧を取得 * @param {string} ticketId - チケットID * @returns {Promise<object>} 添付ファイル一覧 */ async getAttachments(ticketId) { try { logger.info(`チケット(ID: ${ticketId})の添付ファイル一覧を取得します`); const response = await this.makeRequest(`tickets/${ticketId}/attachments`); return response; } catch (error) { logger.error(`チケット(ID: ${ticketId})の添付ファイル一覧の取得に失敗しました: ${error.message}`); return { data: null }; } } /** * チケットの履歴を取得 * @param {string} ticketId - チケットID * @returns {Promise<object>} チケット履歴 */ async getTicketHistory(ticketId) { try { logger.info(`チケット(ID: ${ticketId})の履歴を取得します`); const response = await this.makeRequest(`tickets/${ticketId}/history`); return response; } catch (error) { logger.error(`チケット(ID: ${ticketId})の履歴の取得に失敗しました: ${error.message}`); return { data: null }; } } /** * レコードを取得 * @param {string} module - モジュール名 * @param {object} params - クエリパラメータ * @returns {Promise<object>} レコード一覧 */ async getRecords(module, params = {}) { try { const moduleStr = typeof module === "string" ? module : String(module); logger.info(`${moduleStr}のレコードを取得します`); logger.debug("クエリパラメータ:", params); // デフォルトのパラメータを設定 const defaultParams = { limit: 100, from: 1 }; // パラメータをマージ const mergedParams = { ...defaultParams, ...params }; // クエリパラメータを構築 const queryParams = new URLSearchParams(); Object.entries(mergedParams).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { queryParams.append(key, value.join(",")); } else if (typeof value === "object") { queryParams.append(key, JSON.stringify(value)); } else { queryParams.append(key, value.toString()); } } }); // エンドポイントを構築 const endpoint = `${moduleStr}${queryParams.toString() ? `?${queryParams.toString()}` : ""}`; logger.debug(`最終的なエンドポイント: ${endpoint}`); // リクエストを実行(組織IDを文字列として渡す) const response = await this.makeRequest(endpoint, { organizationId: String(this.organizationId) }); // レスポンスのログ出力 if (response && response.data) { logger.info(`${moduleStr}から${response.data.length}件のレコードを取得しました`); } return response; } catch (error) { logger.error(`${moduleStr}のレコード取得に失敗しました: ${error.message}`, error); return { data: [], info: { count: 0, from: 1, limit: 100, hasMore: false } }; } } /** * 今月のチケット一覧を取得 * @returns {Promise<object>} チケット一覧 */ async getCurrentMonthTickets(organizationId) { try { logger.info("今月のチケット一覧を検索APIで取得します"); // 今月の開始日時・終了日時(UTC)を取得 const now = new Date(); const startOfMonth = new Date(Date.UTC(now.getFullYear(), now.getMonth(), 1, 0, 0, 0)); const endOfMonth = new Date(Date.UTC(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59)); const startDateTime = startOfMonth.toISOString(); const endDateTime = endOfMonth.toISOString(); // criteria生成 const criteria = `(createdTime:between:${startDateTime},${endDateTime})`; logger.info(`検索条件: ${criteria}`); const endpoint = `tickets/search?criteria=${encodeURIComponent(criteria)}`; const response = await this.makeRequest(endpoint, organizationId ? { organizationId } : {}); return response; } catch (error) { logger.error("今月のチケット検索に失敗しました", error); return { error: error.message, status: 500, data: [], info: { count: 0, from: 1, limit: 100, hasMore: false } }; } } // 前月のチケット一覧を取得 async getPreviousMonthTickets(organizationId) { try { logger.info("前月のチケット一覧を検索APIで取得します"); const now = new Date(); // 前月の計算 const prevMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1; const year = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(); const startOfPrevMonth = new Date(Date.UTC(year, prevMonth, 1, 0, 0, 0)); const endOfPrevMonth = new Date(Date.UTC(year, prevMonth + 1, 0, 23, 59, 59)); const startDateTime = startOfPrevMonth.toISOString(); const endDateTime = endOfPrevMonth.toISOString(); const criteria = `(createdTime:between:${startDateTime},${endDateTime})`; logger.info(`前月の検索条件: ${criteria}`); const endpoint = `tickets/search?criteria=${encodeURIComponent(criteria)}`; const response = await this.makeRequest(endpoint, organizationId ? { organizationId } : {}); return response; } catch (error) { logger.error("前月のチケット検索に失敗しました", error); return { error: error.message, status: 500, data: [], info: { count: 0, from: 1, limit: 100, hasMore: false } }; } } // 任意の年月のチケット一覧を取得 async getTicketsByMonth(year, month, organizationId) { try { logger.info(`指定月のチケット一覧を検索APIで取得します: ${year}-${month}`); // month: 1-12 const m = Number(month) - 1; const y = Number(year); const startOfMonth = new Date(Date.UTC(y, m, 1, 0, 0, 0)); const endOfMonth = new Date(Date.UTC(y, m + 1, 0, 23, 59, 59)); const startDateTime = startOfMonth.toISOString(); const endDateTime = endOfMonth.toISOString(); const criteria = `(createdTime:between:${startDateTime},${endDateTime})`; logger.info(`指定月の検索条件: ${criteria}`); const endpoint = `tickets/search?criteria=${encodeURIComponent(criteria)}`; const response = await this.makeRequest(endpoint, organizationId ? { organizationId } : {}); return response; } catch (error) { logger.error("指定月のチケット検索に失敗しました", error); return { error: error.message, status: 500, data: [], info: { count: 0, from: 1, limit: 100, hasMore: false } }; } } } // シングルトンインスタンスをエクスポート const zohoDeskClient = new ZohoDeskClient(); export { zohoDeskClient }; export default { zohoDeskClient };