UNPKG

windsurfmcp-aws-mcp-server1

Version:

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

1,663 lines (1,502 loc) 68.4 kB
#!/usr/bin/env node // 公式MCPサーバー実装(ESモジュール形式)- Zoho API統合版 import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { zohoConfig } from './config/zoho-config.mjs'; import { tokenManager } from './utils/token-manager.mjs'; import { logger, logMessage } from './utils/logger.mjs'; import { zohoCRMClient } from './api/zoho-crm.mjs'; import { zohoDeskClient } from './api/zoho-desk.mjs'; import { zohoBooksClient } from './api/zoho-books.mjs'; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import fetch from "node-fetch"; import dotenv from "dotenv"; import http from "http"; import url from "url"; // 環境変数を読み込む dotenv.config(); // __dirnameの代替(ESモジュールでは使用できないため) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // 設定ファイルのパスを確認 const configPath = 'C:\\Users\\Owner\\AppData\\Roaming\\Claude\\claude_desktop_config.json'; if (fs.existsSync(configPath)) { const configData = fs.readFileSync(configPath, 'utf8'); const desktopConfig = JSON.parse(configData); logger.info(`設定ファイルを読み込みました: ${configPath}`); logger.debug(`設定ファイルの内容: ${JSON.stringify(desktopConfig, null, 2)}`); } else { logger.error(`設定ファイルが見つかりません: ${configPath}`); } // 環境設定 const isDevelopment = process.env.NODE_ENV !== 'production'; const useMockTokens = isDevelopment && process.env.USE_MOCK_TOKENS === 'true'; logger.info(`実行モード: ${isDevelopment ? '開発' : '本番'}`); logger.info(`モックトークン使用: ${useMockTokens ? '有効' : '無効'}`); // 標準出力への直接のログ出力を抑制する関数 function setupConsoleRedirect() { const originalConsoleLog = console.log; console.log = function() { const args = Array.from(arguments).map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg) ); logger.debug(args.join(' ')); }; console._originalLog = originalConsoleLog; } // Zoho接続状態を取得する関数 async function getZohoConnectionStatus() { logger.info('Zoho接続状態を取得しています...'); try { // トークンの存在確認で接続状態を判断 const crmConnected = await tokenManager.hasValidToken('crm'); const deskConnected = await tokenManager.hasValidToken('desk'); const booksConnected = await tokenManager.hasValidToken('books'); const result = { crm: { connected: crmConnected, lastUpdated: crmConnected ? new Date().toISOString() : null, authUrl: !crmConnected ? await generateAuthUrl('crm') : null }, desk: { connected: deskConnected, lastUpdated: deskConnected ? new Date().toISOString() : null, authUrl: !deskConnected ? await generateAuthUrl('desk') : null }, books: { connected: booksConnected, lastUpdated: booksConnected ? new Date().toISOString() : null, authUrl: !booksConnected ? await generateAuthUrl('books') : null } }; logger.info(`Zoho接続状態: CRM=${crmConnected}, Desk=${deskConnected}, Books=${booksConnected}`); return result; } catch (error) { logger.error(`Zoho接続状態の取得に失敗しました: ${error.message}`); // エラーが発生した場合はデフォルト値を返す return { crm: { connected: false, lastUpdated: null, authUrl: await generateAuthUrl('crm') }, desk: { connected: false, lastUpdated: null, authUrl: await generateAuthUrl('desk') }, books: { connected: false, lastUpdated: null, authUrl: await generateAuthUrl('books') } }; } } // ZohoCRMからデータを取得する関数 async function getZohoData(params) { const { module, filters = {} } = params; const moduleStr = typeof module === 'string' ? module : String(module); logger.info(`ZohoCRMから${moduleStr}データを取得しています...`); try { // 取得するフィールドを設定 const fields = [ 'Owner', 'Created_Time', 'Modified_Time', 'Deal_Name', 'Amount', 'Stage', 'Closing_Date', 'Account_Name', 'Contact_Name', 'Description', 'Pipeline', 'Probability', 'Expected_Revenue', 'Lead_Source', 'Type' ]; // リクエストパラメータを構築 const requestParams = { page: 1, per_page: 200, fields: fields.join(',') }; // フィルターがある場合は追加 if (Object.keys(filters).length > 0) { Object.assign(requestParams, filters); } logger.debug('リクエストパラメータ:', requestParams); // データを取得 const result = await zohoCRMClient.getRecords(moduleStr, requestParams); logger.info(`ZohoCRMの${moduleStr}データ取得が完了しました`); // 結果をテキスト形式で整形 let formattedResult = `ZohoCRM ${moduleStr}データ:\n\n`; if (result && result.data && Array.isArray(result.data) && result.data.length > 0) { formattedResult += `合計: ${result.data.length}件\n\n`; result.data.forEach((item, index) => { formattedResult += `【${index + 1}】\n`; Object.keys(item).forEach(key => { const value = item[key]; if (typeof value === 'object' && value !== null) { formattedResult += `${key}:\n`; Object.keys(value).forEach(subKey => { formattedResult += ` ${subKey}: ${JSON.stringify(value[subKey])}\n`; }); } else { formattedResult += `${key}: ${JSON.stringify(value)}\n`; } }); formattedResult += '\n'; }); if (result.info) { formattedResult += `ページ情報: ${result.info.page}/${Math.ceil(result.info.count / result.info.perPage)}ページ (全${result.info.count}件)\n`; formattedResult += `次のページがあるか: ${result.info.moreRecords ? 'はい' : 'いいえ'}\n`; } } else { formattedResult += '該当するデータはありません。'; } const responseObj = { content: [{ type: 'text', text: formattedResult }] }; console.error('[DEBUG][getZohoData] return:', JSON.stringify(responseObj)); return responseObj; } catch (error) { logger.error(`ZohoCRMの${moduleStr}データ取得中にエラーが発生しました: ${error.message}`); const errorObj = { content: [{ type: 'text', text: `エラーが発生しました: ${error.message}` }] }; console.error('[DEBUG][getZohoData] return:', JSON.stringify(errorObj)); return errorObj; } const fallbackObj = { content: [{ type: 'text', text: '不明なエラーが発生しました。' }] }; console.error('[DEBUG][getZohoData] return:', JSON.stringify(fallbackObj)); return fallbackObj; } // Zoho Deskからデータを取得する関数 async function getZohoDeskData(params) { const { module, filters = {} } = params; logger.info(`Zoho Deskから${module}データを取得しています...`); try { // 組織IDを取得(configから取得) const organizationId = zohoConfig.getServiceConfig('desk').organizationId; logger.info(`使用する組織ID: ${organizationId}`); // filtersがJSON文字列として渡された場合はパースする let parsedFilters = filters || {}; if (typeof parsedFilters === 'string') { try { parsedFilters = JSON.parse(parsedFilters); logger.info(`フィルターをパースしました: ${JSON.stringify(parsedFilters)}`); } catch (parseError) { logger.error(`フィルターのパースに失敗しました: ${parseError.message}`); parsedFilters = {}; } } // 日付フィルターの処理 if (parsedFilters && (parsedFilters.date_start || parsedFilters.date_end)) { logger.info(`日付フィルターが指定されています: 開始=${parsedFilters.date_start}, 終了=${parsedFilters.date_end}`); } logger.info(`使用するフィルター: ${JSON.stringify(parsedFilters)}`); const result = await zohoDeskClient.getData(module, organizationId, parsedFilters); logger.info(`Zoho Deskの${module}データ取得が完了しました`); // レスポンスの整形 return { content: [{ type: 'text', text: `Zoho Desk ${module}データ:\n${JSON.stringify(result, null, 2)}` }] }; } catch (error) { logger.error(`Zoho Deskの${module}データ取得中にエラーが発生しました: ${error.message}`); return { content: [{ type: 'text', text: `エラーが発生しました: ${error.message}` }] }; } } // Zoho Booksからデータを取得する関数 async function getZohoBooksData(params) { const { module, filters = {} } = params; logger.info(`Zoho Booksから${module}データを取得しています...`); try { // 組織IDを取得(configから取得) const organizationId = zohoConfig.getServiceConfig('books').organizationId; logger.info(`使用する組織ID: ${organizationId}`); // filtersがJSON文字列として渡された場合はパースする let parsedFilters = filters || {}; if (typeof parsedFilters === 'string') { try { parsedFilters = JSON.parse(parsedFilters); logger.info(`フィルターをパースしました: ${JSON.stringify(parsedFilters)}`); } catch (parseError) { logger.error(`フィルターのパースに失敗しました: ${parseError.message}`); parsedFilters = {}; } } // 日付フィルターの処理 if (parsedFilters && (parsedFilters.date_start || parsedFilters.date_end)) { logger.info(`日付フィルターが指定されています: 開始=${parsedFilters.date_start}, 終了=${parsedFilters.date_end}`); } logger.info(`使用するフィルター: ${JSON.stringify(parsedFilters)}`); const result = await zohoBooksClient.getData(module, organizationId, parsedFilters); logger.info(`Zoho Booksの${module}データ取得が完了しました`); // レスポンスの整形 return { content: [{ type: 'text', text: `Zoho Books ${module}データ:\n${JSON.stringify(result, null, 2)}` }] }; } catch (error) { logger.error(`Zoho Booksの${module}データ取得中にエラーが発生しました: ${error.message}`); return { content: [{ type: 'text', text: `エラーが発生しました: ${error.message}` }] }; } } /** * 今月の請求書データを取得 * @param {object} params - パラメータ * @returns {Promise<object>} 請求書データ */ async function getCurrentMonthInvoices(params = {}) { try { logger.info('Zoho Booksから今月の請求書データを取得します...'); // アクセストークンを取得 const accessToken = await tokenManager.getValidAccessToken('books'); logger.info(`アクセストークンを取得しました: ${accessToken.substring(0, 10)}...`); // 組織IDを取得 let organizationId = params.orgId; if (!organizationId) { organizationId = zohoConfig.getServiceConfig('books').organizationId; logger.info(`設定ファイルから組織IDを取得しました: ${organizationId}`); } logger.info(`使用する組織ID: ${organizationId}`); // APIエンドポイントを取得 const apiBaseUrl = 'https://www.zohoapis.com/books/v3'; logger.info(`使用するAPIエンドポイント: ${apiBaseUrl}`); // 今月の日付範囲を計算 const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; // 日付フォーマットをYYYY-MM-DDに変換 const formatDate = (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; // 今月の最初の日と最後の日を取得 const firstDay = new Date(year, month - 1, 1); const lastDay = new Date(year, month, 0); const startDate = formatDate(firstDay); const endDate = formatDate(lastDay); logger.info(`期間: ${startDate} から ${endDate}`); // フィルターを設定 - APIの仕様に合わせて正しいパラメータを使用 const filters = { date_start: startDate, date_end: endDate }; logger.info(`フィルター: ${JSON.stringify(filters)}`); // 請求書データを取得 logger.info('Zoho Booksから請求書データを取得します...'); const invoicesData = await zohoBooksClient.getData('invoices', organizationId, filters); logger.info(`Zoho Booksのinvoicesデータ取得が完了しました`); // 請求書データを整形 const invoices = invoicesData.invoices || []; logger.info(`請求書データ取得が完了しました: ${invoices.length}件`); // レスポンスを作成 - 簡潔な表示に改善 let responseText = `Zoho Books 今月の請求書データ:\n\n`; responseText += `使用した組織ID: ${organizationId}\n`; responseText += `対象期間: ${startDate} から ${endDate}\n\n`; if (invoices.length > 0) { responseText += `合計: ${invoices.length}件\n\n`; // 各請求書の情報を追加 invoices.forEach((invoice, index) => { responseText += `【${index + 1}】\n`; responseText += `請求書番号: ${invoice.invoice_number}\n`; responseText += `顧客名: ${invoice.customer_name}\n`; responseText += `金額: ${invoice.currency_symbol}${invoice.total}\n`; responseText += `残高: ${invoice.currency_symbol}${invoice.balance}\n`; responseText += `日付: ${invoice.date}\n`; responseText += `期限: ${invoice.due_date}\n`; responseText += `ステータス: ${invoice.status}\n\n`; }); } else { responseText += '今月の請求書データはありません。'; } return { content: [ { type: 'text', text: responseText } ] }; } catch (error) { logger.error(`Zoho Booksの請求書データ取得中にエラーが発生しました: ${error.message}`); return { content: [ { type: 'text', text: `エラーが発生しました: ${error.message}` } ] }; } } // Zoho認証URLを生成する関数 async function generateAuthUrl(service) { logger.info(`${service}の認証URLを生成しています...`); try { // zohoConfigから設定を取得 if (!zohoConfig) { throw new Error('zohoConfigが初期化されていません'); } // 設定の有効性を確認 if (!zohoConfig.isConfigValid(service)) { throw new Error(`${service}の設定が無効です。clientId、clientSecret、redirectUriを確認してください`); } const authUrl = zohoConfig.generateAuthUrl(service); logger.info(`${service}の認証URLを生成しました: ${authUrl}`); return authUrl; } catch (error) { logger.error(`${service}の認証URL生成中にエラーが発生しました: ${error.message}`); throw error; } } // 認証コードを処理 async function handleAuthCode(service, code) { try { logger.info(`${service}の認証コードを処理します: ${code}`); // サービス設定を取得 const config = zohoConfig.getServiceConfig(service); // トークンURLを取得 const tokenUrl = zohoConfig.getTokenUrl(service); // トークンリクエストのパラメータを設定 const params = new URLSearchParams({ code: code, client_id: config.clientId, client_secret: config.clientSecret, redirect_uri: config.redirectUri, grant_type: 'authorization_code' }); // トークンリクエストを送信 const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }); // レスポンスを解析 if (!response.ok) { const errorText = await response.text(); logger.error(`認証エラー: ${response.status} ${errorText}`); return { success: false, error: `認証エラー: ${response.status} ${errorText}` }; } const tokenData = await response.json(); // エラーチェック if (tokenData.error) { logger.error(`認証エラー: ${tokenData.error}`); return { success: false, error: tokenData.error }; } // 現在のタイムスタンプを追加 tokenData.created_at = Math.floor(Date.now() / 1000); // トークンを保存 await tokenManager.saveTokens(service, tokenData); logger.info(`${service}の認証が完了しました`); return { success: true, message: `${service}の認証が完了しました` }; } catch (error) { logger.error(`認証処理エラー: ${error.message}`); return { success: false, error: error.message }; } } // Zoho認証情報をリセットする関数 async function resetZohoAuth() { try { logger.info('Zoho認証情報のリセットを開始します'); // すべてのトークンを削除 await tokenManager.deleteAllTokens(); logger.info('Zoho認証情報のリセットが完了しました'); return { success: true, message: 'Zoho認証情報のリセットが完了しました' }; } catch (error) { logger.error(`認証情報リセットエラー: ${error.message}`); return { success: false, error: error.message }; } } // サーバー起動時にトークンの有効性を確認する関数 async function validateAndRefreshTokens() { try { logger.info('トークンの有効性を確認しています...'); const services = ['crm', 'desk', 'books']; for (const service of services) { try { // トークンの有効性をチェック const isValid = await tokenManager.hasValidToken(service); if (isValid) { logger.info(`${service}の有効なトークンが見つかりました`); } else { // リフレッシュトークンが存在するか確認 const hasToken = await tokenManager.hasRefreshToken?.(service); if (hasToken) { logger.info(`${service}のアクセストークンが期限切れ。リフレッシュを試みます...`); if (typeof tokenManager.refreshAccessToken === 'function') { await tokenManager.refreshAccessToken(service); logger.info(`${service}のトークンをリフレッシュしました`); } else { logger.info(`${service}のトークンリフレッシュ機能が利用できません`); } } else { logger.info(`${service}の有効なリフレッシュトークンがありません。再認証が必要です`); } } } catch (err) { logger.error(`${service}トークンの検証中にエラーが発生しました: ${err.message}`); } } logger.info('トークン検証処理が完了しました'); } catch (error) { logger.error(`トークン検証中に一般エラーが発生しました: ${error.message}`); } } // Zoho認証URLを取得する関数 async function getZohoAuthUrl(params) { try { if (!params || !params.service) { throw new Error('serviceパラメータが必要です'); } const service = params.service; logger.info(`${service}の認証URLを取得します`); const authUrl = await generateAuthUrl(service); logger.info(`${service}の認証URLを生成しました: ${authUrl}`); return { success: true, auth_url: authUrl }; } catch (error) { logger.error(`認証URL取得エラー: ${error.message}`); return { success: false, error: error.message }; } } // Zoho認証コードを処理する関数 async function handleZohoAuthCode(service, code) { try { logger.info(`${service}の認証コードを処理します`); const result = await handleAuthCode(service, code); if (!result.success) { throw new Error(result.error); } logger.info(`${service}の認証コード処理が完了しました`); return { success: true, message: result.message }; } catch (error) { logger.error(`認証コード処理エラー: ${error.message}`); return { success: false, error: error.message }; } } // モックトークン生成処理 async function generateMockTokensIfNeeded() { if (!useMockTokens) { logger.info('モックトークン生成はスキップされました(本番モードまたは明示的に無効化)'); return; } logger.info('モックトークンの生成を開始します'); const services = ['crm', 'desk', 'books']; for (const service of services) { try { const hasToken = await tokenManager.hasValidToken(service); if (!hasToken) { logger.info(`${service}の有効なトークンがないため、モックトークンを生成します`); const mockToken = { access_token: `mock_token_${service}_${Date.now()}`, refresh_token: `mock_refresh_${service}_${Date.now()}`, expires_in: 3600, created_at: Math.floor(Date.now() / 1000) }; await tokenManager.saveTokens(service, mockToken); logger.info(`${service}のモックトークンを生成しました`); } else { logger.info(`${service}の有効なトークンが既に存在します`); } } catch (error) { logger.error(`${service}のモックトークン生成中にエラーが発生しました: ${error.message}`); } } } /** * Zoho Booksの銀行口座情報を取得 * @param {string} organizationId - 組織ID * @param {object} filters - 検索フィルター * @returns {Promise<object>} 銀行口座情報 */ export async function getZohoBooksBankAccounts(organizationId, filters = {}) { try { logger.info("Zoho Booksの銀行口座情報を取得します"); const response = await zohoBooksClient.getBankAccounts(organizationId, filters); return { code: 0, message: "success", data: response.bankaccounts || [] }; } catch (error) { logger.error(`Zoho Booksの銀行口座情報取得に失敗しました: ${error.message}`, error); return { code: 1, message: error.message, data: [] }; } } /** * Zoho Booksの勘定科目情報を取得 * @param {string} organizationId - 組織ID * @param {object} filters - 検索フィルター * @returns {Promise<object>} 勘定科目情報 */ export async function getZohoBooksChartOfAccounts(organizationId, filters = {}) { try { logger.info("Zoho Booksの勘定科目情報を取得します"); const response = await zohoBooksClient.getChartOfAccounts(organizationId, filters); return { code: 0, message: "success", data: response.chartofaccounts || [] }; } catch (error) { logger.error(`Zoho Booksの勘定科目情報取得に失敗しました: ${error.message}`, error); return { code: 1, message: error.message, data: [] }; } } /** * Zoho Booksの為替調整情報を取得 * @param {string} organizationId - 組織ID * @param {object} filters - 検索フィルター * @returns {Promise<object>} 為替調整情報 */ export async function getZohoBooksBaseCurrencyAdjustments(organizationId, filters = {}) { try { logger.info("Zoho Booksの為替調整情報を取得します"); const response = await zohoBooksClient.getBaseCurrencyAdjustments(organizationId, filters); return { code: 0, message: "success", data: response.adjustments || [] }; } catch (error) { logger.error(`Zoho Booksの為替調整情報取得に失敗しました: ${error.message}`, error); return { code: 1, message: error.message, data: [] }; } } async function main() { try { // 標準出力へのログ出力をリダイレクト setupConsoleRedirect(); logger.info("Zoho統合MCPサーバーを初期化中..."); // Claude Desktop設定ファイルを読み込む const configPath = 'C:\\Users\\Owner\\AppData\\Roaming\\Claude\\claude_desktop_config.json'; let config; try { if (fs.existsSync(configPath)) { const configData = fs.readFileSync(configPath, 'utf8'); config = JSON.parse(configData); logger.info("Claude Desktop設定ファイルを読み込みました"); } else { throw new Error("Claude Desktop設定ファイルが見つかりません"); } } catch (configError) { logger.error(`設定ファイルの読み込みに失敗しました: ${configError.message}`); throw configError; } // モックトークンの生成 await generateMockTokensIfNeeded(); // トークンの有効性確認は本番モードでのみ実行 if (!isDevelopment) { await validateAndRefreshTokens(); } // サーバーの作成 const server = new McpServer({ name: "ZohoIntegratedMCP", version: "1.0.0" }); // Zoho接続状態取得ツールを登録 server.tool( "get_zoho_connection_status", {}, async () => { const result = await getZohoConnectionStatus(); return { content: [ { type: "text", text: `Zoho接続状態:\n${JSON.stringify(result, null, 2)}` } ] }; } ); // Zoho認証URL取得ツールを登録 server.tool( "get_zoho_auth_url", { service: z.string().describe("認証するサービス名(crm, desk, books)") }, async (params) => { const result = await getZohoAuthUrl(params); return { content: [ { type: "text", text: `${result.auth_url}` } ] }; } ); // Zoho認証コード処理ツールを登録 server.tool( "handle_zoho_auth_code", { service: z.string().describe("認証するサービス名(crm, desk, books)"), code: z.string().describe("認証コード") }, async (params) => { await handleZohoAuthCode(params.service, params.code); return { content: [ { type: "text", text: `${params.service}の認証が完了しました。これでAPIを使用できます。` } ] }; } ); // Zoho認証リセットツールを登録 server.tool( "reset_zoho_auth", {}, async () => { await resetZohoAuth(); return { content: [ { type: "text", text: `Zohoの認証情報をリセットしました。再度認証が必要です。` } ] }; } ); // ZohoCRMデータ取得ツールを登録 server.tool( "get_zoho_data", { module: z.string().describe("取得するデータのモジュール名(leads, contacts等)"), filters: z.record(z.any()).optional().describe("検索フィルター") }, async (params) => { // getZohoDataの戻り値({content: [...]})をそのまま返すことでtype/contentの不整合を防止 return await getZohoData(params); } ); // Zoho Deskデータ取得ツールを登録 server.tool( "get_zohodesk_data", { module: z.string().describe("取得するデータのモジュール名(tickets, contacts等)"), filters: z.record(z.any()).optional().describe("検索フィルター") }, async (params) => { try { const result = await getZohoDeskData(params); return { content: [ { type: "text", text: `Zoho Desk ${params.module}データ:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (err) { console.error(`[Zoho Deskデータ取得エラー]`, err); return { content: [ { type: "text", text: `Zoho Deskデータ取得エラー: ${err.message || err}` } ] }; } } ); // 今月のチケット取得ツールを登録 server.tool( "get_current_month_tickets", {}, async () => { const result = await zohoDeskClient.getCurrentMonthTickets(); return { content: [ { type: "text", text: `Zoho Desk 今月のチケットデータ:\n${JSON.stringify(result, null, 2)}` } ] }; } ); // 前月のチケット取得ツールを登録 server.tool( "get_previous_month_tickets", { orgId: z.string().describe("組織ID") }, async (params) => { // 組織IDが未指定または不正な場合はconfigから取得 // 常にconfigからDeskの組織IDを使用 const orgId = zohoConfig.getServiceConfig('desk').organizationId; const result = await getPreviousMonthTickets(orgId); return { content: [ { type: "text", text: `Zoho Desk 前月のチケットデータ:\n${JSON.stringify(result, null, 2)}` } ] }; } ); // 特定月のチケット取得ツールを登録 server.tool( "get_tickets_by_month", { orgId: z.string().describe("組織ID"), year: z.string().describe("年(例:2024)"), month: z.string().describe("月(1〜12)") }, async (params) => { const result = await getTicketsByMonth(params.orgId, params.year, params.month); return { content: [ { type: "text", text: `Zoho Desk ${params.year}年${params.month}月のチケットデータ:\n${JSON.stringify(result, null, 2)}` } ] }; } ); // Zoho Booksデータ取得ツールを登録 server.tool( "get_zohobooks_data", { module: z.string().describe("取得するデータのモジュール名(invoices, customers等)"), filters: z.record(z.any()).optional().describe("検索フィルター") }, async (params) => { // 組織IDが未指定または不正な場合はconfigから取得 // 常にconfigからBooksの組織IDを使用 const orgId = zohoConfig.getServiceConfig('books').organizationId; const newParams = { ...params, orgId }; const result = await getZohoBooksData(newParams); return { content: [ { type: "text", text: `Zoho Books ${params.module}データ:\n${JSON.stringify(result, null, 2)}` } ] }; } ); // 今月の請求書取得ツールを登録 server.tool( "get_current_month_invoices", { orgId: z.string().optional().describe("組織ID(省略可能、省略時は設定ファイルの値を使用)") }, async (params) => { const result = await getCurrentMonthInvoices(params?.orgId); return { content: [ { type: "text", text: `Zoho Books 今月の請求書データ:\n${JSON.stringify(result, null, 2)}` } ] }; } ); // Zoho Booksの銀行口座情報取得ツールを登録 server.tool( "get_zohobooks_bank_accounts", { orgId: z.string().describe("組織ID"), filters: z.record(z.any()).optional().describe("検索フィルター") }, async (params) => { try { const result = await getZohoBooksBankAccounts(params.orgId, params.filters || {}); return { content: [ { type: "text", text: `Zoho Booksの銀行口座情報:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (error) { logger.error("銀行口座情報取得エラー:", error); return { content: [ { type: "text", text: `銀行口座情報取得エラー: ${error.message}` } ] }; } } ); // Zoho Booksの勘定科目情報取得ツールを登録 server.tool( "get_zohobooks_chart_of_accounts", { orgId: z.string().describe("組織ID"), filters: z.record(z.any()).optional().describe("検索フィルター") }, async (params) => { try { const result = await getZohoBooksChartOfAccounts(params.orgId, params.filters || {}); return { content: [ { type: "text", text: `Zoho Booksの勘定科目情報:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (error) { logger.error("勘定科目情報取得エラー:", error); return { content: [ { type: "text", text: `勘定科目情報取得エラー: ${error.message}` } ] }; } } ); // Zoho Booksの為替調整情報取得ツールを登録 server.tool( "get_zohobooks_base_currency_adjustments", { orgId: z.string().describe("組織ID"), filters: z.record(z.any()).optional().describe("検索フィルター") }, async (params) => { try { const result = await getZohoBooksBaseCurrencyAdjustments(params.orgId, params.filters || {}); return { content: [ { type: "text", text: `Zoho Booksの為替調整情報:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (error) { logger.error("為替調整情報取得エラー:", error); return { content: [ { type: "text", text: `為替調整情報取得エラー: ${error.message}` } ] }; } } ); // CRM: 任意モジュールのレコード作成 server.tool( "zoho_crm_create_record", { module: z.string().describe("作成対象のモジュール名(例: Leads, Contacts等)"), data: z.record(z.any()).describe("作成するレコードのデータ") }, async (params) => { try { if (!zohoCRMClient.createRecord) { throw new Error("CRMレコード作成APIが未実装です"); } const result = await zohoCRMClient.createRecord(params.module, params.data); const responseObj = { content: [ { type: "text", text: `ZohoCRMレコード作成結果:\n${JSON.stringify(result, null, 2)}` } ] }; console.error('[DEBUG][zoho_crm_create_record] return:', JSON.stringify(responseObj)); return responseObj; } catch (err) { const errorObj = { content: [ { type: "text", text: `CRMレコード作成エラー: ${err.message || err}` } ] }; console.error('[DEBUG][zoho_crm_create_record] return:', JSON.stringify(errorObj)); return errorObj; } } ); // Desk: チケット作成 server.tool( "zoho_desk_create_ticket", { data: z.record(z.any()).describe("作成するチケットのデータ") }, async (params) => { try { if (!zohoDeskClient.createTicket) { throw new Error("Deskチケット作成APIが未実装です"); } // 必須フィールドの定義 const requiredFields = { subject: "件名", departmentId: "部門ID", email: "メールアドレス", description: "説明", priority: "優先度", status: "ステータス" }; // 不足している必須フィールドをチェック const missingFields = Object.entries(requiredFields) .filter(([field]) => !params.data[field]) .map(([_, label]) => label); if (missingFields.length > 0) { const errorMessage = `Deskチケット作成エラー: 以下の必須項目が不足しています → ${missingFields.join(', ')}\n\n必須項目の説明:\n` + Object.entries(requiredFields) .map(([_, label]) => `- ${label}`) .join('\n'); const errorObj = { content: [ { type: "text", text: errorMessage } ] }; console.error('[DEBUG][zoho_desk_create_ticket] missing:', JSON.stringify(missingFields)); return errorObj; } // API仕様に準拠したフィールドのみを送信 const ticketData = { subject: params.data.subject, departmentId: params.data.departmentId, email: params.data.email, description: params.data.description, priority: params.data.priority, status: params.data.status }; // オプショナルフィールドの追加 if (params.data.contactId) ticketData.contactId = params.data.contactId; if (params.data.contact) ticketData.contact = params.data.contact; if (params.data.notes) ticketData.notes = params.data.notes; // ログ出力強化 console.error('[DEBUG][zoho_desk_create_ticket] sending:', JSON.stringify(ticketData)); try { const result = await zohoDeskClient.createTicket(ticketData); if (result && result.code && result.code !== 0 && result.message) { // APIからエラーが返却された場合はそのままクライアントへ const errorObj = { content: [ { type: "text", text: `Desk APIエラー: ${result.message}` } ] }; console.error('[DEBUG][zoho_desk_create_ticket] api error:', JSON.stringify(result)); return errorObj; } const responseObj = { content: [ { type: "text", text: `ZohoDeskチケット作成結果:\n${JSON.stringify(result, null, 2)}` } ] }; console.error('[DEBUG][zoho_desk_create_ticket] return:', JSON.stringify(responseObj)); return responseObj; } catch (err) { const errorObj = { content: [ { type: "text", text: `Deskチケット作成エラー: ${err.message || err}` } ] }; console.error('[DEBUG][zoho_desk_create_ticket] catch:', JSON.stringify(errorObj)); return errorObj; } } catch (err) { const errorObj = { content: [ { type: "text", text: `Deskチケット作成エラー: ${err.message || err}` } ] }; console.error('[DEBUG][zoho_desk_create_ticket] return:', JSON.stringify(errorObj)); return errorObj; } } ); // Books: 見積書作成 server.tool( "zoho_books_create_estimate", { orgId: z.string().describe("組織ID"), data: z.record(z.any()).describe("作成する見積書のデータ") }, async (params) => { try { if (!zohoBooksClient.createEstimate) { throw new Error("Books見積書作成APIが未実装です"); } // 組織IDの取得 const organizationId = params.orgId || zohoConfig.getServiceConfig('books').organizationId; if (!organizationId) { throw new Error("組織IDが指定されていません"); } // リクエストデータのログ出力 logger.info(`見積書作成リクエスト: ${JSON.stringify(params.data, null, 2)}`); // APIリクエストを実行 const result = await zohoBooksClient.createEstimate(organizationId, params.data); // レスポンスのログ出力 logger.info(`見積書作成レスポンス: ${JSON.stringify(result, null, 2)}`); // エラーレスポンスの処理 if (result.code && result.code !== 0) { return { content: [ { type: "text", text: `Books見積書作成エラー: ${result.message}` } ] }; } return { content: [ { type: "text", text: `ZohoBooks見積書作成結果:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (err) { logger.error(`見積書作成エラー: ${err.message}`, err); return { content: [ { type: "text", text: `Books見積書作成エラー: ${err.message}` } ] }; } } ); // Books: 受取請求書作成 server.tool( "zoho_books_create_bill", { orgId: z.string().describe("組織ID"), data: z.record(z.any()).describe("作成する受取請求書のデータ") }, async (params) => { try { if (!zohoBooksClient.createBill) { throw new Error("Books受取請求書作成APIが未実装です"); } // 組織IDの取得 const organizationId = params.orgId || zohoConfig.getServiceConfig('books').organizationId; if (!organizationId) { throw new Error("組織IDが指定されていません"); } // リクエストデータのログ出力 logger.info(`受取請求書作成リクエスト: ${JSON.stringify(params.data, null, 2)}`); // APIリクエストを実行 const result = await zohoBooksClient.createBill(organizationId, params.data); // レスポンスのログ出力 logger.info(`受取請求書作成レスポンス: ${JSON.stringify(result, null, 2)}`); // エラーレスポンスの処理 if (result.code && result.code !== 0) { return { content: [ { type: "text", text: `Books受取請求書作成エラー: ${result.message}` } ] }; } return { content: [ { type: "text", text: `ZohoBooks受取請求書作成結果:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (err) { logger.error(`受取請求書作成エラー: ${err.message}`, err); return { content: [ { type: "text", text: `Books受取請求書作成エラー: ${err.message}` } ] }; } } ); // Books: 請求書作成 server.tool( "zoho_books_create_invoice", { orgId: z.string().describe("組織ID"), data: z.object({ customer_id: z.string().describe("顧客ID"), line_items: z.array(z.object({ item_id: z.string().describe("商品ID"), name: z.string().describe("商品名"), quantity: z.number().describe("数量"), rate: z.number().describe("単価") })).describe("明細項目"), date: z.string().describe("請求書日付(YYYY-MM-DD形式)"), due_date: z.string().optional().describe("支払期限(YYYY-MM-DD形式)"), reference_number: z.string().optional().describe("参照番号"), description: z.string().optional().describe("請求書の説明"), exchange_rate: z.number().optional().describe("為替レート"), discount: z.number().optional().describe("割引額"), is_discount_before_tax: z.boolean().optional().describe("税抜き割引かどうか"), discount_type: z.string().optional().describe("割引タイプ(item_level, document_level)"), shipping_charge: z.number().optional().describe("送料"), adjustment: z.number().optional().describe("調整額"), adjustment_description: z.string().optional().describe("調整の説明"), notes: z.string().optional().describe("備考"), terms: z.string().optional().describe("支払条件"), salesperson_name: z.string().optional().describe("営業担当者名"), custom_fields: z.array(z.object({ label: z.string().describe("カスタムフィールド名"), value: z.any().describe("カスタムフィールド値") })).optional().describe("カスタムフィールド") }).describe("請求書データ") }, async (params) => { try { if (!zohoBooksClient.createInvoice) { throw new Error("Books請求書作成APIが未実装です"); } // 組織IDの取得 const organizationId = params.orgId || zohoConfig.getServiceConfig('books').organizationId; if (!organizationId) { throw new Error("組織IDが指定されていません"); } // リクエストデータのログ出力 logger.info(`請求書作成リクエスト: ${JSON.stringify(params.data, null, 2)}`); // APIリクエストを実行 const result = await zohoBooksClient.createInvoice(organizationId, params.data); // レスポンスのログ出力 logger.info(`請求書作成レスポンス: ${JSON.stringify(result, null, 2)}`); // エラーレスポンスの処理 if (result.code && result.code !== 0) { return { content: [ { type: "text", text: `Books請求書作成エラー: ${result.message}` } ] }; } return { content: [ { type: "text", text: `ZohoBooks請求書作成結果:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (err) { logger.error(`請求書作成エラー: ${err.message}`, err); return { content: [ { type: "text", text: `Books請求書作成エラー: ${err.message}` } ] }; } } ); // Books: 請求書支払い作成 server.tool( "zoho_books_create_customer_payment", { orgId: z.string().describe("組織ID"), data: z.object({ customer_id: z.string().describe("顧客ID"), payment_mode: z.string().describe("支払い方法(check, cash, creditcard, banktransfer, bankremittance, autotransaction, others)"), amount: z.number().describe("支払い金額"), date: z.string().describe("支払い日付(YYYY-MM-DD形式)"), reference_number: z.string().optional().describe("参照番号"), description: z.string().optional().describe("支払いの説明"), invoices: z.array(z.object({ invoice_id: z.string().describe("請求書ID"), amount_applied: z.number().describe("支払い金額") })).describe("支払い対象の請求書リスト"), exchange_rate: z.number().optional().describe("為替レート"), bank_charges: z.number().optional().describe("銀行手数料"), account_id: z.string().optional().describe("入金口座ID") }).describe("支払いデータ") }, async (params) => { try { logger.info("請求書支払い作成リクエスト:", { orgId: params.orgId, data: params.data }); const result = await zohoBooksClient.createCustomerPayment(params.orgId, params.data); logger.info("請求書支払い作成レスポンス:", result); return { content: [ { type: "text", text: `ZohoBooks請求書支払い作成結果:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (error) { logger.error("請求書支払い作成エラー:", error); return { content: [ { type: "text", text: `Books請求書支払い作成エラー: ${error.message}` } ] }; } } ); // Books: 受取請求書支払い作成 server.tool( "zoho_books_create_vendor_payment", { orgId: z.string().describe("組織ID"), data: z.object({ vendor_id: z.string().describe("仕入先ID"), amount: z.number().describe("支払い金額"), paid_through_account_id: z.string().describe("支払い元の口座ID"), payment_mode: z.string().describe("支払い方法"), description: z.string().optional().describe("支払いの説明"), reference_number: z.string().optional().describe("参照番号"), bills: z.array(z.object({ bill_id: z.string().describe("受取請求書ID"), amount_applied: z.number().describe("支払い金額") })).describe("支払い対象の受取請求書リスト") }).describe("支払いデータ") }, async (params) => { try { logger.info("受取請求書支払い作成リクエスト:", { orgId: params.orgId, data: params.data }); const result = await zohoBooksClient.createVendorPayment(params.orgId, params.data); logger.info("受取請求書支払い作成レスポンス:", result); return { content: [ { type: "text", text: `ZohoBooks受取請求書支払い作成結果:\n${JSON.stringify(result, null, 2)}` } ] }; } catch (error) { logger.error("受取請求書支払い作成エラー:", error); return { content: [ { type: "text", text: `Books受取請求書支払い作成エラー: ${error.message}` } ] }; } } ); logger.info("MCPサーバーを初期化しました"); // 標準入出力トランスポートを作成 const transport = new StdioServerTransport(); logger.info("標準入出力トランスポートを初期化しました"); logger.info("サーバーを接続しています..."); // サーバーを接続 await server.connect(transport); logger.info("サーバーが正常に接続されました"); // コールバックサーバーを起動(トークン取得用) const callbackPort = config.server?.port || 3005; try { const callbackServer = http.createServer((req, res) => { handleCallbackRequest(req, res); }); callbackServer.on('error', (err) => { if (err.code === 'EADDRINUSE') { logger.info(`ポート${callbackPort}は既に使用されています。コールバックサーバーは起動しませんが、既存のサーバーを使用します。`); } else { logger.error(`コールバックサーバーの起動中にエラーが発生しました: ${err.message}`); } }); callbackServer.listen(callbackPort, () => { logger.info(`コールバックサーバーがポート${callbackPort}で起動しました`); }); } catch (serverError) { logger.error(`コールバックサーバーの起動に失敗しました: ${serverError.message}`); // コールバックサーバーの失敗はMCPサーバーには影響しないので続行 } } catch (error) { logger.error(`エラーが発生しました: ${error.message}`); logger.error(`スタックトレース: ${error.stack}`); process.exit(1); } } // プロセスが終了しないようにする process.on('SIGINT', () => { logger.info('SIGINTシグナルを受信しました'); // シグナルを無視 }); process.on('SIGTERM', () => { logger.info('SIGTERMシグナルを受信しました'); // シグナルを無視 }); // 未処理の例外をキャッチ process.on('uncaughtException', (error) => { logger.error(`未処理の例外が発生しました: ${error.message}`); logger.error(`スタックトレース: ${error.stack}`); // プロセスは終了させない }); // メインプロセスを実行 main().catch(error => { logger.error(`致命的なエラーが発生しました: ${error.message}`); logger.error(`スタックトレース: ${error.stack}`); process.exit(1); }); // メッセージを処理する関数 async function handleMessage(message) { try { // 受信したメッセージをログに出力(メソッド種別によってレベルを切り替え) let methodForLog = null; try { const parsedTmp = typeof message === 'string' ? JSON.parse(message) : {}; methodForLog = parsedTmp.method; } catch(e) {} if (methodForLog === 'resources/list' || methodForLog === 'prompts/list') { logger.debug(`受信したメッセージ: ${typeof message === 'string' ? message : 'Message is not a string'}`); } else { logger.info(`受信したメッセージ: ${typeof message === 'string' ? message : 'Message is not a string'}`); } // メッセージの前処理 if (typeof message !== 'string') { throw new Error('メッセージは文字列である必要があります'); } let cleanMessage = message.trim(); // メッセージが空の場合はエラーを返す if (!cleanMessage) { logger.info('空のメッセージを受信しました'); return JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: '空のメッセージ' } }); } // メッセージをJSONとしてパース let request; try { request = JSON.parse(cleanMessage); logger.info(`パースされたリクエスト: ${JSON.stringify(request, null, 2)}`); } catch (parseError) { logger.error(`JSONパースエラー: ${parseError.message}`); logger.error(`問題のメッセージ: ${cleanMessage}`); return JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: `Invalid JSON: ${parseError.message}` } }); } // リクエストの基本構造を検証 if (!request.jsonrpc || request.jsonrpc !== '2.0') { logger.error('[ERROR] Invalid jsonrpc version:', request.jsonrpc); return JSON.stringify({ jsonrpc: '2.0', id: request.id || null, error: { code: -32600, message: 'Invalid Request: jsonrpc must be "2.0"' } }); } if (!request.method) { logger.error('[ERROR] Method is missing in the request'); return JSON.stringify({ jsonrpc: '2.0', id: request.id || null, error: { code