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